From 490e87eae1eb0b8a00f966040e5507432d469e70 Mon Sep 17 00:00:00 2001 From: danielsbsantos Date: Wed, 24 Jun 2026 14:22:33 +0100 Subject: [PATCH] Add routing and blob The transport channels routing and blob modules have been created. The communication modes supported are CAN, FlexRay and nPDU (aka PduTransport). --- CMakeLists.txt | 2 + docker/development/files/requirements.lock | 4 +- .../referenceApp/application/CMakeLists.txt | 8 + .../include/systems/RoutingSystem.h | 187 +++++ .../referenceApp/application/src/app/app.cpp | 17 +- .../application/src/logger/logger.cpp | 5 + .../application/src/systems/RoutingSystem.cpp | 188 +++++ .../referenceApp/configuration/CMakeLists.txt | 15 + .../configuration/include/blob/ConfigType.h | 29 + .../include/blob/configuration.h | 44 + .../configuration/include/routing/channelId.h | 22 + .../configuration/include/routing/constants.h | 27 + .../referenceApp/configuration/routing.jsonl | 4 + libs/bsw/CMakeLists.txt | 3 + libs/bsw/blob/CMakeLists.txt | 13 + libs/bsw/blob/doc/api.rst | 34 + libs/bsw/blob/doc/configuration.rst | 112 +++ libs/bsw/blob/doc/design.rst | 92 ++ libs/bsw/blob/doc/examples.rst | 100 +++ libs/bsw/blob/doc/generator.rst | 81 ++ libs/bsw/blob/doc/index.rst | 30 + libs/bsw/blob/doc/integration.rst | 21 + libs/bsw/blob/include/blob/Blob.h | 102 +++ libs/bsw/blob/include/blob/Config.h | 46 + libs/bsw/blob/include/blob/Header.h | 36 + libs/bsw/blob/include/blob/Logger.h | 14 + libs/bsw/blob/include/blob/util.h | 49 ++ libs/bsw/blob/module.spec | 0 libs/bsw/blob/src/blob/Blob.cpp | 65 ++ libs/bsw/blob/src/blob/Config.cpp | 72 ++ libs/bsw/blob/src/blob/Header.cpp | 68 ++ libs/bsw/blob/src/blob/Logger.cpp | 12 + libs/bsw/blob/src/blob/util.cpp | 49 ++ libs/bsw/blob/test/CMakeLists.txt | 12 + libs/bsw/blob/test/include/blob/ConfigType.h | 29 + .../blob/test/include/blob/configuration.h | 155 ++++ libs/bsw/blob/test/routing.jsonl | 29 + libs/bsw/blob/test/src/BlobTest.cpp | 511 +++++++++++ libs/bsw/routing/CMakeLists.txt | 30 + libs/bsw/routing/doc/design.rst | 56 ++ libs/bsw/routing/doc/index.rst | 67 ++ libs/bsw/routing/doc/integration.rst | 174 ++++ libs/bsw/routing/doc/interface.rst | 144 ++++ .../routing/doc/resources/routing_example.dot | 384 +++++++++ libs/bsw/routing/include/io/udp/Receiver.h | 126 +++ libs/bsw/routing/include/io/udp/Sender.h | 58 ++ .../routing/include/routing/ChannelNames.h | 39 + .../routing/include/routing/ClampedCounter.h | 59 ++ .../routing/include/routing/ErrorHandler.h | 76 ++ libs/bsw/routing/include/routing/Header.h | 50 ++ .../bsw/routing/include/routing/Integration.h | 510 +++++++++++ .../routing/include/routing/LegacyRxAdapter.h | 67 ++ .../routing/include/routing/LegacyTxAdapter.h | 108 +++ libs/bsw/routing/include/routing/Logger.h | 13 + .../routing/include/routing/PduRoutingTable.h | 59 ++ .../routing/PduTransportBufferedWriter.h | 124 +++ .../include/routing/PduTransportConfig.h | 50 ++ .../include/routing/PduTransportIntegration.h | 392 +++++++++ .../include/routing/PduTransportRxAdapter.h | 247 ++++++ .../include/routing/PduTransportTxAdapter.h | 71 ++ libs/bsw/routing/include/routing/Router.h | 177 ++++ libs/bsw/routing/include/routing/RxAdapter.h | 119 +++ .../routing/include/routing/RxAdapterTable.h | 57 ++ .../routing/include/routing/TxAdapterTable.h | 44 + libs/bsw/routing/include/routing/pduRouting.h | 36 + libs/bsw/routing/include/routing/util.h | 30 + libs/bsw/routing/module.spec | 1 + libs/bsw/routing/src/routing/ChannelNames.cpp | 80 ++ libs/bsw/routing/src/routing/Header.cpp | 65 ++ .../routing/src/routing/LegacyTxAdapter.cpp | 212 +++++ libs/bsw/routing/src/routing/Logger.cpp | 12 + .../routing/PduTransportBufferedWriter.cpp | 119 +++ .../src/routing/PduTransportConfig.cpp | 98 +++ .../src/routing/PduTransportTxAdapter.cpp | 76 ++ libs/bsw/routing/src/routing/RxAdapter.cpp | 193 +++++ .../routing/src/routing/RxAdapterTable.cpp | 55 ++ .../routing/src/routing/TxAdapterTable.cpp | 46 + libs/bsw/routing/src/routing/pduRouting.cpp | 138 +++ libs/bsw/routing/src/routing/util.cpp | 46 + libs/bsw/routing/test/CMakeLists.txt | 41 + .../routing/test/include/blob/ConfigType.h | 29 + .../routing/test/include/blob/configuration.h | 180 ++++ .../routing/test/include/routing/channelId.h | 24 + .../routing/test/include/routing/constants.h | 25 + .../routing/test/include/routing/definition.h | 136 +++ libs/bsw/routing/test/routing.jsonl | 34 + .../routing/test/src/io/udp/ReceiverTest.cpp | 384 +++++++++ .../routing/test/src/io/udp/SenderTest.cpp | 114 +++ .../test/src/routing/ChannelNamesTest.cpp | 126 +++ .../test/src/routing/ClampedCounterTest.cpp | 114 +++ .../routing/test/src/routing/HeaderTest.cpp | 198 +++++ .../test/src/routing/IntegrationTest.cpp | 792 ++++++++++++++++++ .../test/src/routing/LegacyRxAdapterTest.cpp | 64 ++ .../test/src/routing/LegacyTxAdapterTest.cpp | 593 +++++++++++++ .../test/src/routing/PduRoutingTableTest.cpp | 117 +++ .../PduTransportBufferedWriterTest.cpp | 423 ++++++++++ .../src/routing/PduTransportConfigTest.cpp | 185 ++++ .../routing/PduTransportIntegrationTest.cpp | 751 +++++++++++++++++ .../src/routing/PduTransportRxAdapterTest.cpp | 770 +++++++++++++++++ .../src/routing/PduTransportTxAdapterTest.cpp | 265 ++++++ .../routing/test/src/routing/RouterTest.cpp | 706 ++++++++++++++++ .../test/src/routing/RxAdapterTableTest.cpp | 159 ++++ .../test/src/routing/RxAdapterTest.cpp | 589 +++++++++++++ .../test/src/routing/TxAdapterTableTest.cpp | 131 +++ .../routing/test/src/routing/definition.cpp | 149 ++++ tools/blob/NOTICE.md | 20 + tools/blob/__init__.py | 85 ++ tools/blob/__main__.py | 330 ++++++++ tools/blob/requirements.txt | 1 + tools/blob/routing/__init__.py | 12 + tools/blob/routing/__main__.py | 258 ++++++ tools/blob/routing/channel.py | 110 +++ tools/blob/routing/table.py | 310 +++++++ tools/blob/routing/utils.py | 144 ++++ tools/blob/utils.py | 101 +++ 115 files changed, 15262 insertions(+), 3 deletions(-) create mode 100644 executables/referenceApp/application/include/systems/RoutingSystem.h create mode 100644 executables/referenceApp/application/src/systems/RoutingSystem.cpp create mode 100644 executables/referenceApp/configuration/include/blob/ConfigType.h create mode 100644 executables/referenceApp/configuration/include/blob/configuration.h create mode 100644 executables/referenceApp/configuration/include/routing/channelId.h create mode 100644 executables/referenceApp/configuration/include/routing/constants.h create mode 100644 executables/referenceApp/configuration/routing.jsonl create mode 100644 libs/bsw/blob/CMakeLists.txt create mode 100644 libs/bsw/blob/doc/api.rst create mode 100644 libs/bsw/blob/doc/configuration.rst create mode 100644 libs/bsw/blob/doc/design.rst create mode 100644 libs/bsw/blob/doc/examples.rst create mode 100644 libs/bsw/blob/doc/generator.rst create mode 100644 libs/bsw/blob/doc/index.rst create mode 100644 libs/bsw/blob/doc/integration.rst create mode 100644 libs/bsw/blob/include/blob/Blob.h create mode 100644 libs/bsw/blob/include/blob/Config.h create mode 100644 libs/bsw/blob/include/blob/Header.h create mode 100644 libs/bsw/blob/include/blob/Logger.h create mode 100644 libs/bsw/blob/include/blob/util.h create mode 100644 libs/bsw/blob/module.spec create mode 100644 libs/bsw/blob/src/blob/Blob.cpp create mode 100644 libs/bsw/blob/src/blob/Config.cpp create mode 100644 libs/bsw/blob/src/blob/Header.cpp create mode 100644 libs/bsw/blob/src/blob/Logger.cpp create mode 100644 libs/bsw/blob/src/blob/util.cpp create mode 100644 libs/bsw/blob/test/CMakeLists.txt create mode 100644 libs/bsw/blob/test/include/blob/ConfigType.h create mode 100644 libs/bsw/blob/test/include/blob/configuration.h create mode 100644 libs/bsw/blob/test/routing.jsonl create mode 100644 libs/bsw/blob/test/src/BlobTest.cpp create mode 100644 libs/bsw/routing/CMakeLists.txt create mode 100644 libs/bsw/routing/doc/design.rst create mode 100644 libs/bsw/routing/doc/index.rst create mode 100644 libs/bsw/routing/doc/integration.rst create mode 100644 libs/bsw/routing/doc/interface.rst create mode 100644 libs/bsw/routing/doc/resources/routing_example.dot create mode 100644 libs/bsw/routing/include/io/udp/Receiver.h create mode 100644 libs/bsw/routing/include/io/udp/Sender.h create mode 100644 libs/bsw/routing/include/routing/ChannelNames.h create mode 100644 libs/bsw/routing/include/routing/ClampedCounter.h create mode 100644 libs/bsw/routing/include/routing/ErrorHandler.h create mode 100644 libs/bsw/routing/include/routing/Header.h create mode 100644 libs/bsw/routing/include/routing/Integration.h create mode 100644 libs/bsw/routing/include/routing/LegacyRxAdapter.h create mode 100644 libs/bsw/routing/include/routing/LegacyTxAdapter.h create mode 100644 libs/bsw/routing/include/routing/Logger.h create mode 100644 libs/bsw/routing/include/routing/PduRoutingTable.h create mode 100644 libs/bsw/routing/include/routing/PduTransportBufferedWriter.h create mode 100644 libs/bsw/routing/include/routing/PduTransportConfig.h create mode 100644 libs/bsw/routing/include/routing/PduTransportIntegration.h create mode 100644 libs/bsw/routing/include/routing/PduTransportRxAdapter.h create mode 100644 libs/bsw/routing/include/routing/PduTransportTxAdapter.h create mode 100644 libs/bsw/routing/include/routing/Router.h create mode 100644 libs/bsw/routing/include/routing/RxAdapter.h create mode 100644 libs/bsw/routing/include/routing/RxAdapterTable.h create mode 100644 libs/bsw/routing/include/routing/TxAdapterTable.h create mode 100644 libs/bsw/routing/include/routing/pduRouting.h create mode 100644 libs/bsw/routing/include/routing/util.h create mode 100644 libs/bsw/routing/module.spec create mode 100644 libs/bsw/routing/src/routing/ChannelNames.cpp create mode 100644 libs/bsw/routing/src/routing/Header.cpp create mode 100644 libs/bsw/routing/src/routing/LegacyTxAdapter.cpp create mode 100644 libs/bsw/routing/src/routing/Logger.cpp create mode 100644 libs/bsw/routing/src/routing/PduTransportBufferedWriter.cpp create mode 100644 libs/bsw/routing/src/routing/PduTransportConfig.cpp create mode 100644 libs/bsw/routing/src/routing/PduTransportTxAdapter.cpp create mode 100644 libs/bsw/routing/src/routing/RxAdapter.cpp create mode 100644 libs/bsw/routing/src/routing/RxAdapterTable.cpp create mode 100644 libs/bsw/routing/src/routing/TxAdapterTable.cpp create mode 100644 libs/bsw/routing/src/routing/pduRouting.cpp create mode 100644 libs/bsw/routing/src/routing/util.cpp create mode 100644 libs/bsw/routing/test/CMakeLists.txt create mode 100644 libs/bsw/routing/test/include/blob/ConfigType.h create mode 100644 libs/bsw/routing/test/include/blob/configuration.h create mode 100644 libs/bsw/routing/test/include/routing/channelId.h create mode 100644 libs/bsw/routing/test/include/routing/constants.h create mode 100644 libs/bsw/routing/test/include/routing/definition.h create mode 100644 libs/bsw/routing/test/routing.jsonl create mode 100644 libs/bsw/routing/test/src/io/udp/ReceiverTest.cpp create mode 100644 libs/bsw/routing/test/src/io/udp/SenderTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/ChannelNamesTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/ClampedCounterTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/HeaderTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/IntegrationTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/LegacyRxAdapterTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/LegacyTxAdapterTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/PduRoutingTableTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/PduTransportBufferedWriterTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/PduTransportConfigTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/PduTransportIntegrationTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/PduTransportRxAdapterTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/PduTransportTxAdapterTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/RouterTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/RxAdapterTableTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/RxAdapterTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/TxAdapterTableTest.cpp create mode 100644 libs/bsw/routing/test/src/routing/definition.cpp create mode 100644 tools/blob/NOTICE.md create mode 100644 tools/blob/__init__.py create mode 100644 tools/blob/__main__.py create mode 100644 tools/blob/requirements.txt create mode 100644 tools/blob/routing/__init__.py create mode 100644 tools/blob/routing/__main__.py create mode 100644 tools/blob/routing/channel.py create mode 100644 tools/blob/routing/table.py create mode 100644 tools/blob/routing/utils.py create mode 100644 tools/blob/utils.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 840515f077c..4c4fe662a38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,6 +158,7 @@ if (BUILD_EXECUTABLE STREQUAL "unitTest") add_subdirectory(libs/bsw/asyncFreeRtos/test) add_subdirectory(libs/bsw/asyncImpl/examples) add_subdirectory(libs/bsw/asyncImpl/test) + add_subdirectory(libs/bsw/blob/test) add_subdirectory(libs/bsw/bsp/test) add_subdirectory(libs/bsw/cpp2can/test) add_subdirectory(libs/bsw/cpp2ethernet/test) @@ -171,6 +172,7 @@ if (BUILD_EXECUTABLE STREQUAL "unitTest") add_subdirectory(libs/bsw/lwipSocket/test) add_subdirectory(libs/bsw/middleware/test) add_subdirectory(libs/bsw/platform/test) + add_subdirectory(libs/bsw/routing/test) add_subdirectory(libs/bsw/runtime/test) add_subdirectory(libs/bsw/storage/test) add_subdirectory(libs/bsw/timer/test) diff --git a/docker/development/files/requirements.lock b/docker/development/files/requirements.lock index bf419073064..08b230372d2 100644 --- a/docker/development/files/requirements.lock +++ b/docker/development/files/requirements.lock @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --output-file=docker/development/files/requirements.lock docker/development/files/requirements.txt test/pyTest/requirements.txt tools/UdsTool/requirements.txt +# pip-compile --output-file=docker/development/files/requirements.lock docker/development/files/requirements.txt test/pyTest/requirements.txt tools/UdsTool/requirements.txt tools/blob/requirements.txt # anyio==4.12.0 # via httpx @@ -31,6 +31,8 @@ cmakelang==0.6.13 # via -r docker/development/files/requirements.txt crashtest==0.4.1 # via cleo +crc==7.1.0 + # via -r tools/blob/requirements.txt cryptography==46.0.3 # via secretstorage distlib==0.4.0 diff --git a/executables/referenceApp/application/CMakeLists.txt b/executables/referenceApp/application/CMakeLists.txt index 96ff384f916..c21156e7e48 100644 --- a/executables/referenceApp/application/CMakeLists.txt +++ b/executables/referenceApp/application/CMakeLists.txt @@ -23,6 +23,10 @@ if (PLATFORM_SUPPORT_ETHERNET) target_sources(app.referenceApp PRIVATE src/systems/DoIpServerSystem.cpp) endif () + + if (PLATFORM_SUPPORT_CAN) + target_sources(app.referenceApp PRIVATE src/systems/RoutingSystem.cpp) + endif () endif () if (PLATFORM_SUPPORT_CAN) @@ -103,6 +107,10 @@ if (PLATFORM_SUPPORT_ETHERNET) if (PLATFORM_SUPPORT_TRANSPORT) target_link_libraries(app.referenceApp PRIVATE doip) endif () + + if (PLATFORM_SUPPORT_CAN) + target_link_libraries(app.referenceApp PRIVATE blob routing) + endif () endif () if (PLATFORM_SUPPORT_STORAGE) diff --git a/executables/referenceApp/application/include/systems/RoutingSystem.h b/executables/referenceApp/application/include/systems/RoutingSystem.h new file mode 100644 index 00000000000..20376872ebc --- /dev/null +++ b/executables/referenceApp/application/include/systems/RoutingSystem.h @@ -0,0 +1,187 @@ +/******************************************************************************** + * Copyright (c) 2022 Accenture + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/ErrorHandler.h" + +#include "systems/ICanSystem.h" + +#include +#include // pre-integration testing, to be removed later +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace systems +{ +class RoutingSystem +: public ::etl::singleton_base +, public ::lifecycle::SimpleLifecycleComponent +, private ::async::IRunnable +{ +public: + static constexpr auto MAX_NUM_PDU_TRANSPORT_CHANNELS = ::routing::NUM_PDU_TRANSPORT_CHANNELS; + static constexpr auto NUM_FLEXRAY_CHANNELS = ::routing::NUM_FLEXRAY_CHANNELS; + static constexpr auto NUM_CAN_CHANNELS = ::routing::NUM_CAN_CHANNELS; + + // Ethernet MTU (1500) - IPv4 header size (20) - UDP header size (8) - space for future protocol + // headers (56) + static constexpr size_t MAX_FRAME_SIZE = 1416U; + static constexpr size_t MAX_PDU_TRANSPORT_CHANNEL_ELEMENT_SIZE = 72U; + static constexpr size_t MAX_CAN_CHANNEL_ELEMENT_SIZE = 72U; + static constexpr size_t MAX_FLEXRAY_CHANNEL_ELEMENT_SIZE = 72U; + + RoutingSystem(::async::ContextType context, ::can::ICanSystem& canSystem); + + void init() override; + void run() override; + void shutdown() override; + + ::async::ContextType getTransitionContext(Transition::Type /* transition */) override + { + return _context; + } + +private: + static constexpr size_t MAX_CAN_QUEUE_ELEMENT_SIZE = 72U; + static constexpr size_t CAN_QUEUE_SIZE = 2048U; + using CanQueue = ::io::MemoryQueue; + using CanReader = ::io::MemoryQueueReader; + using CanWriter = ::io::MemoryQueueWriter; + + class CanRxListener final : public ::can::ICANFrameListener + { + public: + CanRxListener(CanWriter& inputWriter) : _canFilter(), _inputWriter(inputWriter) + { + // It will accept any IDs, but Router will filter them. + // Possible optimization: filtering in listener to avoid spamming the input queue. + _canFilter.open(); + } + + void frameReceived(::can::CANFrame const& frame) override + { + ::etl::span pdu = _inputWriter.allocate(8U + frame.getPayloadLength()); + if (pdu.empty()) + { + return; + } + + pdu.take<::etl::be_uint32_t>() = frame.getId(); + pdu.take<::etl::be_uint32_t>() = frame.getPayloadLength(); + ::etl::copy( + ::etl::span(frame.getPayload(), frame.getPayloadLength()), pdu); + + _inputWriter.commit(); + } + + ::can::IFilter& getFilter() override { return _canFilter; } + + private: + ::can::IntervalFilter _canFilter; + + CanWriter& _inputWriter; + }; + + class CanOutputWriter final : public ::io::IWriter + { + public: + CanOutputWriter(::can::ICanTransceiver& canTransceiver) : _canTransceiver(canTransceiver) {} + + size_t maxSize() const override { return MAX_CAN_QUEUE_ELEMENT_SIZE; } + + ::etl::span allocate(size_t size) + { + _currentFrame = ::etl::make_span(_data).first(size); + return _currentFrame; + } + + void commit() + { + if (_currentFrame.empty()) + { + return; + } + + uint32_t id = _currentFrame.take<::etl::be_uint32_t>(); + uint32_t length = _currentFrame.take<::etl::be_uint32_t>(); + auto payload = _currentFrame; + ::can::CANFrame frame(id, payload.data(), length); + + // No retry handling + (void)_canTransceiver.write(frame); + + _currentFrame = {}; + } + + void flush() {} + + private: + ::can::ICanTransceiver& _canTransceiver; + + ::etl::array _data; + ::etl::span _currentFrame; + }; + + using Integration = ::routing::Integration< + MAX_NUM_PDU_TRANSPORT_CHANNELS, + NUM_CAN_CHANNELS, + NUM_FLEXRAY_CHANNELS, + MAX_PDU_TRANSPORT_CHANNEL_ELEMENT_SIZE, + MAX_CAN_CHANNEL_ELEMENT_SIZE, + MAX_FLEXRAY_CHANNEL_ELEMENT_SIZE>; + using PduTransportIntegration + = ::routing::PduTransportIntegration; + + void execute() override; + + void + handleError(::routing::ErrorHandler::StatusCode statusCode, uint8_t channelId, uint32_t data); + + bool _initialized; + + PduTransportIntegration _pduTransportIntegration; + + Integration _integration; + + ::async::TimeoutType _timeout; + + ::async::ContextType _context; + + ::can::ICanSystem& _canSystem; + + ::etl::vector<::udp::LwipDatagramSocket, MAX_NUM_PDU_TRANSPORT_CHANNELS> _lwipInputSockets; + ::etl::vector<::udp::LwipDatagramSocket, MAX_NUM_PDU_TRANSPORT_CHANNELS> _lwipOutputSockets; + ::etl::vector<::udp::AbstractDatagramSocket*, MAX_NUM_PDU_TRANSPORT_CHANNELS> _inputSockets; + ::etl::vector<::udp::AbstractDatagramSocket*, MAX_NUM_PDU_TRANSPORT_CHANNELS> _outputSockets; + + ::etl::vector _canInputQueues; + ::etl::vector _canInputReaders; + ::etl::vector _canInputWriters; + ::etl::vector _canRxListeners; + + ::etl::vector _canOutputWriters; + + ::routing::ErrorHandler::Function _errorHandlingFunction; +}; + +} // namespace systems diff --git a/executables/referenceApp/application/src/app/app.cpp b/executables/referenceApp/application/src/app/app.cpp index 860e911f189..50b866da966 100644 --- a/executables/referenceApp/application/src/app/app.cpp +++ b/executables/referenceApp/application/src/app/app.cpp @@ -42,7 +42,11 @@ #include "systems/DoIpServerSystem.h" #endif // PLATFORM_SUPPORT_TRANSPORT #endif // PLATFORM_SUPPORT_ETHERNET - // + +#if defined(PLATFORM_SUPPORT_ETHERNET) && defined(PLATFORM_SUPPORT_CAN) +#include "systems/RoutingSystem.h" +#endif // defined(PLATFORM_SUPPORT_ETHERNET) && defined(PLATFORM_SUPPORT_CAN) + #if defined(PLATFORM_SUPPORT_CAN) && defined(PLATFORM_SUPPORT_TRANSPORT) #include "systems/DoCanSystem.h" #endif // defined(PLATFORM_SUPPORT_CAN) && defined(PLATFORM_SUPPORT_TRANSPORT) @@ -136,6 +140,10 @@ ::etl::typed_storage<::systems::SafetySystem> safetySystem; ::etl::typed_storage<::systems::EthernetSystem> ethernetSystem; #endif // PLATFORM_SUPPORT_ETHERNET +#if defined(PLATFORM_SUPPORT_ETHERNET) && defined(PLATFORM_SUPPORT_CAN) +::etl::typed_storage<::systems::RoutingSystem> routingSystem; +#endif // defined(PLATFORM_SUPPORT_ETHERNET) && defined(PLATFORM_SUPPORT_CAN) + #ifdef PLATFORM_SUPPORT_TRANSPORT ::etl::typed_storage<::transport::TransportSystem> transportSystem; #ifdef PLATFORM_SUPPORT_CAN @@ -309,7 +317,12 @@ void startApp() 6U); #endif - /* runlevel 7 */ +#if defined(PLATFORM_SUPPORT_ETHERNET) && defined(PLATFORM_SUPPORT_CAN) + lifecycleManager.addComponent( + "routing", routingSystem.create(TASK_ETHERNET, ::systems::getCanSystem()), 6U); +#endif + +/* runlevel 7 */ #if defined(PLATFORM_SUPPORT_TRANSPORT) && defined(PLATFORM_SUPPORT_UDS) lifecycleManager.addComponent( "uds", udsSystem.create(lifecycleManager, *transportSystem, TASK_UDS, LOGICAL_ADDRESS), 7U); diff --git a/executables/referenceApp/application/src/logger/logger.cpp b/executables/referenceApp/application/src/logger/logger.cpp index ecaba5387b4..0270e13bfcb 100644 --- a/executables/referenceApp/application/src/logger/logger.cpp +++ b/executables/referenceApp/application/src/logger/logger.cpp @@ -49,6 +49,7 @@ DEFINE_LOGGER_COMPONENT(DOIP); DEFINE_LOGGER_COMPONENT(UDS); DEFINE_LOGGER_COMPONENT(LWIP); DEFINE_LOGGER_COMPONENT(RUST); +DEFINE_LOGGER_COMPONENT(ROUTING); #include #include @@ -88,6 +89,10 @@ LOGGER_COMPONENT_MAPPING_INFO(_DEBUG, LWIP, ::util::format::Color::LIGHT_YELLOW) LOGGER_COMPONENT_MAPPING_INFO(_DEBUG, DOIP, ::util::format::Color::LIGHT_GREEN) #endif // PLATFORM_SUPPORT_TRANSPORT #endif // PLATFORM_SUPPORT_ETHERNET +#if defined(PLATFORM_SUPPORT_ETHERNET) && defined(PLATFORM_SUPPORT_CAN) +LOGGER_COMPONENT_MAPPING_INFO(_DEBUG, ROUTING, ::util::format::Color::LIGHT_CYAN) +#endif // defined(PLATFORM_SUPPORT_ETHERNET) && defined(PLATFORM_SUPPORT_CAN) + /* end: adding logger components */ END_LOGGER_COMPONENT_MAPPING_INFO_TABLE(); diff --git a/executables/referenceApp/application/src/systems/RoutingSystem.cpp b/executables/referenceApp/application/src/systems/RoutingSystem.cpp new file mode 100644 index 00000000000..69dec359c84 --- /dev/null +++ b/executables/referenceApp/application/src/systems/RoutingSystem.cpp @@ -0,0 +1,188 @@ +/******************************************************************************** + * Copyright (c) 2022 Accenture + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "systems/RoutingSystem.h" + +#include +#include +#include + +#include +#include +#include + +namespace systems +{ +namespace logger = ::util::logger; + +RoutingSystem::RoutingSystem(::async::ContextType const context, ::can::ICanSystem& canSystem) +: ::etl::singleton_base{*this} +, _initialized(false) +, _pduTransportIntegration() +, _integration() +, _timeout() +, _context(context) +, _canSystem(canSystem) +, _errorHandlingFunction( + decltype(_errorHandlingFunction)::create(*this)) +{} + +void RoutingSystem::init() +{ + ::etl::span const blob = ::blob::CONFIGURATION_BLOB; + + if (blob.empty()) + { + logger::Logger::error(logger::ROUTING, "Blob is an empty slice"); + + transitionDone(); + + return; + } + + ::etl::vector pduTransportChannelIds; + ::etl::vector<::io::IReader*, MAX_NUM_PDU_TRANSPORT_CHANNELS> pduTransportReaders; + ::etl::vector<::io::IWriter*, MAX_NUM_PDU_TRANSPORT_CHANNELS> pduTransportWriters; + + for (uint8_t i = 0; i < MAX_NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + auto const pduTransportChannelId = Integration::FIRST_PDU_TRANSPORT_CHANNEL_ID + i; + auto const channelConfig + = ::routing::config(blob, ::blob::ConfigType::CHANNEL, pduTransportChannelId); + if (channelConfig.data.empty()) + { + break; + } + + pduTransportChannelIds.emplace_back(pduTransportChannelId); + _inputSockets.emplace_back(&_lwipInputSockets.emplace_back()); + _outputSockets.emplace_back(&_lwipOutputSockets.emplace_back()); + } + + _pduTransportIntegration.init(blob, pduTransportChannelIds, _inputSockets, _outputSockets); + + auto const inputReaders = _pduTransportIntegration.inputReaders(); + auto const bufferedOutputWriters = _pduTransportIntegration.bufferedOutputWriters(); + + for (size_t i = 0; i < pduTransportChannelIds.size(); ++i) + { + pduTransportReaders.emplace_back(&inputReaders[i]); + pduTransportWriters.emplace_back(&bufferedOutputWriters[i]); + } + + ::etl::array<::io::IReader*, NUM_CAN_CHANNELS> canReaders; + ::etl::array<::io::IWriter*, NUM_CAN_CHANNELS> canWriters; + + auto& queue = _canInputQueues.emplace_back(); + auto& reader = _canInputReaders.emplace_back(queue); + auto& inputWriter = _canInputWriters.emplace_back(queue); + (void)_canRxListeners.emplace_back(inputWriter); + auto& outputWriter + = _canOutputWriters.emplace_back(*_canSystem.getCanTransceiver(::busid::CAN_0)); + + canReaders.front() = &reader; + canWriters.front() = &outputWriter; + + _integration.init( + blob, + pduTransportReaders, + pduTransportWriters, + ::routing::canChannelIds, + canReaders, + canWriters, + {}, + {}, + {}, + _errorHandlingFunction); + + _initialized = true; + + transitionDone(); +} + +void RoutingSystem::run() +{ + if (!_initialized) + { + transitionDone(); + + return; + } + + _canSystem.getCanTransceiver(::busid::CAN_0)->addCANFrameListener(_canRxListeners.front()); + + _pduTransportIntegration.activate(::eth1::IP_ADDRESS, 160, _errorHandlingFunction); + + ::async::scheduleAtFixedRate(_context, *this, _timeout, 1U, ::async::TimeUnit::MILLISECONDS); + + transitionDone(); +} + +void RoutingSystem::shutdown() +{ + if (!_initialized) + { + transitionDone(); + + return; + } + + _timeout.cancel(); + + _pduTransportIntegration.disable(); + + _canSystem.getCanTransceiver(::busid::CAN_0)->removeCANFrameListener(_canRxListeners.front()); + + transitionDone(); +} + +void RoutingSystem::execute() +{ + // PduTransportIntegration must run in the Ethernet context because socket API is not + // thread-safe. + _pduTransportIntegration.checkTransmissionTimeouts(); + _pduTransportIntegration.sendUdpFrames(); + + _integration.route(); +} + +void RoutingSystem::handleError( + ::routing::ErrorHandler::StatusCode const statusCode, + uint8_t const /*channelId*/, + uint32_t const /*data*/) +{ + switch (statusCode) + { + case ::routing::ErrorHandler::StatusCode::INVALID_MESSAGE_LENGTH: + logger::Logger::debug(logger::ROUTING, "Invalid message length"); + break; + case ::routing::ErrorHandler::StatusCode::MEM_ALLOCATION_FAILURE: + logger::Logger::debug(logger::ROUTING, "Memory allocation failure"); + break; + case ::routing::ErrorHandler::StatusCode::INVALID_MESSAGE_HEADER_SIZE: + logger::Logger::debug(logger::ROUTING, "Invalid message header size"); + break; + case ::routing::ErrorHandler::StatusCode::INVALID_PARSED_LENGTH: + logger::Logger::debug(logger::ROUTING, "Invalid parsed length"); + break; + case ::routing::ErrorHandler::StatusCode::INVALID_REMOTE_IP_ADDRESS: + logger::Logger::debug(logger::ROUTING, "Invalid remote IP address"); + break; + case ::routing::ErrorHandler::StatusCode::UNKNOWN_MESSAGE_ID: + logger::Logger::debug(logger::ROUTING, "Unknown message ID"); + break; + case ::routing::ErrorHandler::StatusCode::INVALID_PAYLOAD_LENGTH: + logger::Logger::debug(logger::ROUTING, "Invalid payload length"); + break; + default: logger::Logger::debug(logger::ROUTING, "Unknown error"); break; + } +} + +} // namespace systems diff --git a/executables/referenceApp/configuration/CMakeLists.txt b/executables/referenceApp/configuration/CMakeLists.txt index 98862efc49d..09e17b1b87f 100644 --- a/executables/referenceApp/configuration/CMakeLists.txt +++ b/executables/referenceApp/configuration/CMakeLists.txt @@ -3,3 +3,18 @@ add_library(configuration common/src/busid/BusId.cpp) target_include_directories(configuration PUBLIC include) target_link_libraries(configuration PUBLIC commonHeaders async) + +target_link_libraries(configuration INTERFACE common etl) + +add_library(commonImpl common/src/busid/BusId.cpp) + +target_link_libraries(commonImpl PRIVATE configuration async) + +# python -m blob binary -i routing.jsonl -c blob.routing.table | python -m blob +# header data -n CONFIGURATION_BLOB > include/blob/configuration.h + +# python -m blob.routing header ${BLOB_INPUT_FILE} > include/routing/channelId.h + +# python -m blob header config-type > include/blob/ConfigType.h +target_include_directories(blobConfiguration INTERFACE include) +target_include_directories(routingConfiguration INTERFACE include) diff --git a/executables/referenceApp/configuration/include/blob/ConfigType.h b/executables/referenceApp/configuration/include/blob/ConfigType.h new file mode 100644 index 00000000000..9eafa48d8b3 --- /dev/null +++ b/executables/referenceApp/configuration/include/blob/ConfigType.h @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// This is a generated file. Please do not edit it. + +#pragma once + +#include + +namespace blob +{ +enum class ConfigType : uint32_t +{ + ROUTING = 0x00, + RX_ADAPTER = 0x01, + TX_ADAPTER = 0x02, + CHANNEL = 0xCC, + CHANNEL_NAMES = 0xDD, + META = 0xFE, + UNKNOWN = 0xFFFFFFFF +}; +} // namespace blob diff --git a/executables/referenceApp/configuration/include/blob/configuration.h b/executables/referenceApp/configuration/include/blob/configuration.h new file mode 100644 index 00000000000..f8c8c04da6e --- /dev/null +++ b/executables/referenceApp/configuration/include/blob/configuration.h @@ -0,0 +1,44 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// This is a generated file. Please do not edit it. + +#pragma once + +#include + +namespace blob +{ +uint8_t const CONFIGURATION_BLOB[376] = { + 0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x01, 0x6C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0xFF, 0xFF, + 0x0B, 0x26, 0xE8, 0x46, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x09, 0x03, 0xFD, 0x58, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0xDA, 0xA0, 0x14, 0x47, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x3B, 0x66, 0x06, 0x16, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0xF9, 0x64, 0xA7, 0x0B, + 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, + 0x01, 0x11, 0x00, 0xA0, 0x04, 0xD2, 0x09, 0x29, 0x00, 0x04, 0xEF, 0xC0, 0xFF, 0xFD, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0x39, 0x64, 0xF5, 0x13, 0x00, 0x00, 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x14, + 0x43, 0x41, 0x4E, 0x30, 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, + 0x52, 0x54, 0x30, 0x00, 0x4E, 0xD3, 0xAE, 0xEA}; +} // namespace blob diff --git a/executables/referenceApp/configuration/include/routing/channelId.h b/executables/referenceApp/configuration/include/routing/channelId.h new file mode 100644 index 00000000000..771c2ece9c1 --- /dev/null +++ b/executables/referenceApp/configuration/include/routing/channelId.h @@ -0,0 +1,22 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +namespace routing +{ +// clang-format off +static constexpr uint8_t CAN0 = 0; + +static constexpr uint8_t canChannelIds[] = {CAN0}; + +static constexpr uint8_t frChannelIds[] = {}; +// clang-format on +} // namespace routing diff --git a/executables/referenceApp/configuration/include/routing/constants.h b/executables/referenceApp/configuration/include/routing/constants.h new file mode 100644 index 00000000000..56c36a585de --- /dev/null +++ b/executables/referenceApp/configuration/include/routing/constants.h @@ -0,0 +1,27 @@ +// Copyright 2023 Accenture. + +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/channelId.h" + +#include + +namespace routing +{ +static constexpr uint8_t NUM_PDU_TRANSPORT_CHANNELS = 1U; +static constexpr uint8_t NUM_CAN_CHANNELS = sizeof(::routing::canChannelIds); +static constexpr uint8_t NUM_FLEXRAY_CHANNELS = sizeof(::routing::frChannelIds); +static constexpr uint8_t NUM_CHANNELS + = NUM_CAN_CHANNELS + NUM_FLEXRAY_CHANNELS + NUM_PDU_TRANSPORT_CHANNELS; + +} // namespace routing diff --git a/executables/referenceApp/configuration/routing.jsonl b/executables/referenceApp/configuration/routing.jsonl new file mode 100644 index 00000000000..2f77952b23f --- /dev/null +++ b/executables/referenceApp/configuration/routing.jsonl @@ -0,0 +1,4 @@ +{"type": "channel", "value": {"name": "CAN0", "id": 0, "type": "can", "configuration": {}}} +{"type": "channel", "value": {"name": "PDU_TRANSPORT0", "type": "pdu_transport", "configuration": {"connection-type": "multicast", "ip-address": "239.192.255.253", "mode": "rxtx", "local-port": 1234, "remote-port": 2345, "pcp": 0, "vlan-id": 160}}} +{"type":"routing","value":{"input":{"channel-name":"CAN0","message-id":1,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":256,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":257,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"CAN0","message-id":2,"offset":0,"length":8}]}} diff --git a/libs/bsw/CMakeLists.txt b/libs/bsw/CMakeLists.txt index 9443e86689b..8f559bacd8e 100644 --- a/libs/bsw/CMakeLists.txt +++ b/libs/bsw/CMakeLists.txt @@ -21,6 +21,7 @@ endif () if (DEFINED BUILD_TARGET_RTOS AND BUILD_TARGET_RTOS STREQUAL "THREADX") add_subdirectory(asyncThreadX) endif () + add_subdirectory(bsp) add_subdirectory(common) add_subdirectory(cpp2can) @@ -33,12 +34,14 @@ add_subdirectory(timer) add_subdirectory(util) if (OPENBSW_CONFIGURE_GENERIC_BSW) + add_subdirectory(blob) add_subdirectory(cpp2ethernet) add_subdirectory(docan) add_subdirectory(doip) add_subdirectory(loggerIntegration) add_subdirectory(lwipSocket) add_subdirectory(middleware) + add_subdirectory(routing) add_subdirectory(runtime) add_subdirectory(storage) add_subdirectory(transport) diff --git a/libs/bsw/blob/CMakeLists.txt b/libs/bsw/blob/CMakeLists.txt new file mode 100644 index 00000000000..9317ef11b11 --- /dev/null +++ b/libs/bsw/blob/CMakeLists.txt @@ -0,0 +1,13 @@ +add_library( + blob + src/blob/Blob.cpp + src/blob/Config.cpp + src/blob/Header.cpp + src/blob/Logger.cpp + src/blob/util.cpp) + +target_include_directories(blob PUBLIC include) + +add_library(blobConfiguration INTERFACE) + +target_link_libraries(blob PUBLIC blobConfiguration etl util) diff --git a/libs/bsw/blob/doc/api.rst b/libs/bsw/blob/doc/api.rst new file mode 100644 index 00000000000..35cdb79ce8e --- /dev/null +++ b/libs/bsw/blob/doc/api.rst @@ -0,0 +1,34 @@ +.. + ******************************************************************************* + Copyright (c) 2026 BMW AG + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + ******************************************************************************* + +APIs +==== + +.. sourceinclude:: include/blob/Blob.h + :start-after: [Load] + :end-before: [Load] + +Before performing any operation on the blob data, it is important to validate this data. +This is enabled by ``checkHeader()``. Called during blob loading and initialization, +``checkHeader()`` checks the version and magic number of the blob, and assigns the blob's size. +This size is later checked in the constructor of ``Blob``. + +.. sourceinclude:: include/blob/Header.h + :start-after: [HeaderCheck] + :end-before: [HeaderCheck] + +The ``checkCrc()``, also called during blob loading, uses ``etl::crc32`` (standard CRC-32). +It re-computes the CRC on the configuration's data and compares it against the previously +computed CRC bytes at the end of the configuration's data. + +.. sourceinclude:: include/blob/util.h + :start-after: [CrcCheck] + :end-before: [CrcCheck] \ No newline at end of file diff --git a/libs/bsw/blob/doc/configuration.rst b/libs/bsw/blob/doc/configuration.rst new file mode 100644 index 00000000000..44ab034c88e --- /dev/null +++ b/libs/bsw/blob/doc/configuration.rst @@ -0,0 +1,112 @@ +.. + ******************************************************************************* + Copyright (c) 2026 BMW AG + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + ******************************************************************************* + +Configuration +============= + +A blob is a contiguous, self-describing binary region. All multi-byte fields are stored in +big-endian byte order. The region begins with a fixed-size header, followed by the blob data, +which is itself a sequence of configurations. + +Blob layout +----------- + +.. list-table:: + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Size + - Description + * - ``version`` + - 4 bytes + - Blob format version. Currently ``0x00000001``. + * - ``magic`` + - 4 bytes + - Magic number identifying a blob. Always ``0xDEADBEEF``. + * - ``size`` + - 4 bytes + - Size, in bytes, of the blob data that follows the header. + * - ``data`` + - ``size`` + - Concatenated configurations (see below). + +The header is consumed by ``checkHeader()`` (see :ref:`blob` APIs), which verifies the version and +magic number and records the declared data size. The ``Blob`` constructor additionally checks that +the actual memory region matches ``HEADER_SIZE + size``. + +Configuration layout +-------------------- + +Each configuration inside the blob data has its own header followed by a payload. The ``type`` field +identifies how the payload should be interpreted. + +.. list-table:: + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Size + - Description + * - ``type`` + - 4 bytes + - Configuration type identifier (see *Configuration types* below). + * - ``reserved`` + - 8 bytes + - Reserved space, i.e., configuration-specific data that is not interpreted by the blob module. + * - ``size`` + - 4 bytes + - Size, in bytes, of the configuration ``data`` (including padding and the trailing CRC). + * - ``data`` + - ``size`` + - Configuration payload, optionally padded, ending with a 4-byte CRC. + +The last four bytes of every configuration ``data`` hold a big-endian CRC computed over the preceding +payload bytes. The CRC is verified by ``checkCrc()``, which re-computes a 32-bit CRC over the preceding +payload bytes using ``etl::crc32`` (standard CRC-32). The payload is padded with ``0xFF`` bytes so that +its length is a multiple of four before the CRC is appended. + +Configuration types +------------------- + +The supported configuration types are defined by the ``ConfigType`` enum: + +.. list-table:: + :header-rows: 1 + :widths: 25 20 55 + + * - Type + - Value + - Description + * - ``ROUTING`` + - ``0x00`` + - PDU routing table. + * - ``RX_ADAPTER`` + - ``0x01`` + - RX adapter table. + * - ``TX_ADAPTER`` + - ``0x02`` + - TX adapter table. + * - ``CHANNEL`` + - ``0xCC`` + - Channel configuration. + * - ``CHANNEL_NAMES`` + - ``0xDD`` + - Channel name configuration. + * - ``META`` + - ``0xFE`` + - Metaconfiguration: a list of descriptive strings about other configurations. + * - ``UNKNOWN`` + - ``0xFFFFFFFF`` + - Sentinel for an unrecognized configuration type. + +A byte-level example of a complete blob containing a ``CHANNEL_NAMES`` and a ``ROUTING`` +configuration is given in :doc:`examples`. \ No newline at end of file diff --git a/libs/bsw/blob/doc/design.rst b/libs/bsw/blob/doc/design.rst new file mode 100644 index 00000000000..a184541401e --- /dev/null +++ b/libs/bsw/blob/doc/design.rst @@ -0,0 +1,92 @@ +.. + ******************************************************************************* + Copyright (c) 2026 BMW AG + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + ******************************************************************************* + +Module Design +============= + +The ``blob`` module is a small, allocation-free library for reading and validating the binary +gateway tables described in :doc:`configuration`. It never copies the underlying data; +instead it operates on ``::etl::span`` views into the memory region that holds the +blob. + +Components +---------- + +``Header`` + Plain data structure describing the blob header (``version``, ``magic``, ``size``). + ``checkHeader()`` validates the version and magic number and populates the declared data size. + +``Blob`` + Wraps a validated blob span and exposes a forward (input) iterator over its configurations. The + constructor calls ``checkHeader()`` and verifies that the memory region size matches + ``HEADER_SIZE + size``; on failure it logs an error and yields an empty span. Iteration + advances by each configuration's declared size and stops once the span is exhausted. + +``Config`` + Represents a single configuration as a ``type`` and a ``data`` span. ``Config::from_bytes()`` + parses a configuration header (via ``loadConfigHeader()``), rejects ``UNKNOWN`` types and + inconsistent sizes, and returns a span bounded to the configuration's extent. + +``util`` + Free functions for higher-level access: ``load()`` performs full validation, ``config()`` looks + up the first configuration of a given type, ``checkCrc()`` verifies a configuration's trailing + CRC, and the ``loadColumn()`` template safely reinterprets a sized byte run as a typed column. + +Loading and validation flow +--------------------------- + +The recommended entry point is ``load()``. It performs the full sequence of checks and returns a +span that is guaranteed to reference a structurally valid, CRC-verified blob (or an empty span on +failure): + +.. uml:: + + start + :load(mem); + partition checkHeader { + if (mem.size >= HEADER_SIZE?) then (no) + :return false; + elseif (version valid?) then (no) + :return false; + elseif (magic valid?) then (no) + :return false; + else (yes) + :return true; + endif + } + if (checkHeader succeeded?) then (no) + :return empty span; + stop + endif + if (mem.size >= HEADER_SIZE + header.size?) then (no) + :return empty span; + stop + endif + :iterate configurations; + repeat + :checkCrc(config.data); + repeat while (more configurations?) + if (all CRCs valid?) then (no) + :return empty span; + stop + endif + :return blob span; + stop + +Design rationale +---------------- + +* **Zero-copy and allocation-free.** All access is span-based, making the module suitable for + constrained embedded targets and safe to use on read-only memory such as flash. +* **Fail-safe.** Every parsing step is size-checked, and CRC validation guards against corruption. + Invalid input collapses to an empty span rather than producing undefined behaviour. +* **Lazy iteration.** Configurations are decoded on demand by the ``Blob`` iterator, so the cost of + parsing is paid only for the configurations that are actually inspected. \ No newline at end of file diff --git a/libs/bsw/blob/doc/examples.rst b/libs/bsw/blob/doc/examples.rst new file mode 100644 index 00000000000..011ed8ed3d4 --- /dev/null +++ b/libs/bsw/blob/doc/examples.rst @@ -0,0 +1,100 @@ +.. + ******************************************************************************* + Copyright (c) 2026 BMW AG + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + ******************************************************************************* + +Examples +======== + +The following example shows a simplified usage of the ``Blob`` module: + +.. code-block:: cpp + + #include + + #include + #include + #include + + int main() + { + uint8_t const blobData[68] + = {0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x05, 0x1A, 0xFD, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBD, 0xEA, 0x81, 0x89}; + + for (auto const config : ::blob::Blob(blobData)) + { + if (config.type == ::blob::Config::Type::ROUTING) + { + if (::blob::checkCrc(config.data)) + { + std::cout << "This routing table is valid."; + } + else + { + std::cout << "This routing table is invalid."; + } + } + } + + return 0; + } + +In this example, a ``Blob`` instance is created, after which the loop iterates over its +configurations, namely, ``CHANNEL_NAMES: 0xDD`` and ``ROUTING: 0x00``, and produces the output +below. Since the ``ROUTING`` configuration contains valid data, re-computed CRC bytes turn out to be +equal to pre-computed ``CRC bytes: 0xBDEA8189``. + +.. code-block:: + + This routing table is valid. + +Shown below is a more readable representation of ``blobData``: + +``Blob`` + 0x00, 0x00, 0x00, 0x01, ``version`` + + 0xDE, 0xAD, 0xBE, 0xEF, ``magic`` + + ``data`` + 0x00, 0x00, 0x00, 0x38, ``size`` + + ``ChannelNames`` + 0x00, 0x00, 0x00, 0xDD, ``type`` + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ``reserved`` + + ``data`` + 0x00, 0x00, 0x00, 0x04, ``size`` + + 0x05, 0x1A, 0xFD, 0x54, ``crc`` + + ``Routing`` + 0x00, 0x00, 0x00, 0x00, ``type`` + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ``reserved`` + + ``data`` + 0x00, 0x00, 0x00, 0x14, ``size`` + + ``destination offsets`` + 0x00, 0x00, 0x00, 0x04, ``size`` + + 0x00, 0x00, 0x00, 0x00, ``offset`` + + ``output message ids`` + 0x00, 0x00, 0x00, 0x00, ``size`` + + ``destinations`` + 0x00, 0x00, 0x00, 0x00, ``size`` + + 0xBD, 0xEA, 0x81, 0x89, ``crc`` \ No newline at end of file diff --git a/libs/bsw/blob/doc/generator.rst b/libs/bsw/blob/doc/generator.rst new file mode 100644 index 00000000000..4d7ba9c89b1 --- /dev/null +++ b/libs/bsw/blob/doc/generator.rst @@ -0,0 +1,81 @@ +.. + ******************************************************************************* + Copyright (c) 2026 BMW AG + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + ******************************************************************************* + +Generator +========= + +The ``blob`` Python tool located in ``tools/blob`` generates blobs from a JSON Lines (``.jsonl``) +description of the desired configurations. It emits either the binary blob or generated C++ header +files for use in applications. + +Usage +----- + +Invoke the tool as a Python module from the ``tools`` directory so that the ``blob`` package can be +imported: + +.. code-block:: console + + cd tools + python -m blob [options] + +Commands +-------- + +``binary`` + Generates the binary blob from a ``.jsonl`` input and writes it to ``--output`` (or stdout). + Configuration builders are selected with ``--config``, e.g. ``--config blob.routing``. + +``pprint`` + Pretty-prints the blob structure as annotated, commented byte rows, such as those shown in + :doc:`examples`. Useful for inspecting the layout described in :doc:`configuration`. + +``header data`` + Emits a C++ header that embeds the blob as a ``uint8_t`` array (``--name`` controls the array + name, default ``BLOB_DATA``). + +``header config-type`` + Emits a C++ header containing the ``ConfigType`` enum used by the module + (see :doc:`configuration`). + +``header metadata`` + Emits a C++ header containing the ``Metadata`` enum derived from the ``meta`` entries in the + input. + +Each command accepts ``-i``/``--input`` (defaulting to stdin) and ``-o``/``--output`` +(defaulting to stdout). + +Example +------- + +Generate a binary blob containing routing information and embed it into a C++ header: + +.. code-block:: console + + cd tools + python -m blob binary --config blob.routing -i routing.jsonl -o blob.bin + python -m blob header data -i blob.bin -o BlobData.h + +The generator computes the per-configuration CRC and ``0xFF`` padding automatically, so the +resulting blob passes the validation performed by ``load()`` and ``checkCrc()`` at runtime. + +Routing helper +-------------- + +The ``blob.routing`` subpackage provides an auxiliary CLI for working with routing tables: + +.. code-block:: console + + cd tools + python -m blob.routing [options] + +It can sort routings by channel ID, pretty-print them, render a Graphviz ``.dot`` visualization of +the routing graph, and generate a C++ header containing the routing channel IDs. \ No newline at end of file diff --git a/libs/bsw/blob/doc/index.rst b/libs/bsw/blob/doc/index.rst new file mode 100644 index 00000000000..5965b94c6e8 --- /dev/null +++ b/libs/bsw/blob/doc/index.rst @@ -0,0 +1,30 @@ +.. + ******************************************************************************* + Copyright (c) 2026 BMW AG + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + ******************************************************************************* + +.. _blob: + +blob +==== + +This module loads from memory the binary representation of a generated gateway table, so as to +facilitate easy and structured interpretation of the data contained. It also verifies the +correctness of this data, otherwise referred to as the 'blob' and further organized into +'configurations'. + +.. toctree:: + :maxdepth: 2 + + integration + configuration + generator + api + examples + design \ No newline at end of file diff --git a/libs/bsw/blob/doc/integration.rst b/libs/bsw/blob/doc/integration.rst new file mode 100644 index 00000000000..161d23dc1de --- /dev/null +++ b/libs/bsw/blob/doc/integration.rst @@ -0,0 +1,21 @@ +.. + ******************************************************************************* + Copyright (c) 2026 BMW AG + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + ******************************************************************************* + +Integration +=========== + +The module defines the API ``load()``, which converts a pointer in memory to a span of data, after +sufficiently checking that this data is that of a blob. + +Alternatively, the ``Blob`` class can be instantiated with a blob span, so as to neatly iterate over +configurations using the class's custom iterator implementation. However, it is recommended that the +blob span returned by ``load()`` be used for any further processing in a given module, and that the +``Blob`` iterator be constructed only on demand. \ No newline at end of file diff --git a/libs/bsw/blob/include/blob/Blob.h b/libs/bsw/blob/include/blob/Blob.h new file mode 100644 index 00000000000..8a8a02c3be0 --- /dev/null +++ b/libs/bsw/blob/include/blob/Blob.h @@ -0,0 +1,102 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "blob/Config.h" +#include "blob/Header.h" +#include "blob/Logger.h" + +#include +#include +#include + +namespace blob +{ +namespace logger = ::util::logger; + +class Blob +{ +public: + static constexpr uint32_t VERSION = 1U; + static constexpr uint32_t MAGIC = 0xDEADBEEFU; + static constexpr size_t HEADER_SIZE = sizeof(Header); + + struct Iterator : ::etl::iterator + { + explicit Iterator(::etl::span const b) : _b(b) {} + + value_type operator*() const { return Config::from_bytes(_b); } + + value_type operator->() const { return **this; } + + Iterator& operator++() + { + auto const config = **this; + _b.advance(config.data.size()); + if ((_b.empty()) || (config.data.empty())) + { + _b = {}; + } + return *this; + } + + bool operator==(Iterator const& other) const + { + return (_b.size() == other._b.size()) && (_b.data() == other._b.data()); + } + + bool operator!=(Iterator const& other) const { return !(*this == other); } + + private: + ::etl::span _b; + }; + + explicit Blob(::etl::span const b) : _blob() + { + ::blob::Header header; + + if (!::blob::checkHeader(b, header)) + { + logger::Logger::error(logger::BLOB, "Failed to init blob due to invalid header"); + _blob = {}; + + return; + } + + auto const blobSize = header.size; + if ((b.size() - HEADER_SIZE) != blobSize) + { + logger::Logger::error( + logger::BLOB, + "Failed to init blob due to invalid blob size (expected=%u,actual=%u)", + static_cast(blobSize), + static_cast(b.size() - HEADER_SIZE)); + + _blob = {}; + + return; + } + + _blob = b.subspan(HEADER_SIZE); + } + + Iterator begin() const { return Iterator(_blob); } + + Iterator end() const { return Iterator({}); } + +private: + ::etl::span _blob; +}; + +// [Load] +::etl::span load(::etl::span const mem); +// [Load] +} // namespace blob diff --git a/libs/bsw/blob/include/blob/Config.h b/libs/bsw/blob/include/blob/Config.h new file mode 100644 index 00000000000..93583bad8b5 --- /dev/null +++ b/libs/bsw/blob/include/blob/Config.h @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "blob/ConfigType.h" + +#include + +namespace blob +{ +struct Config +{ + using Type = ::blob::ConfigType; + + struct __attribute__((packed)) Header + { + Type configType = Type::UNKNOWN; + uint64_t reserved = 0U; + uint32_t size = 0U; + }; + + bool operator==(Config const& other) const + { + return (type == other.type) && (data.size() == other.data.size()) + && (data.data() == other.data.data()); + } + + Config* operator->() { return this; } + + static Config from_bytes(::etl::span const mem); + + Type type = Type::UNKNOWN; + ::etl::span data; +}; + +Config::Header loadConfigHeader(::etl::span& mem); + +} // namespace blob diff --git a/libs/bsw/blob/include/blob/Header.h b/libs/bsw/blob/include/blob/Header.h new file mode 100644 index 00000000000..ca7c058626c --- /dev/null +++ b/libs/bsw/blob/include/blob/Header.h @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +/** + * This file defines and provides functions to interact with the blob's header. + */ + +#include + +namespace blob +{ + +struct __attribute__((packed)) Header +{ + uint32_t version = 0U; + uint32_t magic = 0U; + uint32_t size = 0U; +}; + +/** + * Given blob span, checks the blob's version and magic number. + */ +// [HeaderCheck] +bool checkHeader(::etl::span blob, Header& header); +// [HeaderCheck] + +} // namespace blob diff --git a/libs/bsw/blob/include/blob/Logger.h b/libs/bsw/blob/include/blob/Logger.h new file mode 100644 index 00000000000..0d248cec13d --- /dev/null +++ b/libs/bsw/blob/include/blob/Logger.h @@ -0,0 +1,14 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include +DECLARE_LOGGER_COMPONENT(BLOB) diff --git a/libs/bsw/blob/include/blob/util.h b/libs/bsw/blob/include/blob/util.h new file mode 100644 index 00000000000..f7de6d08cc1 --- /dev/null +++ b/libs/bsw/blob/include/blob/util.h @@ -0,0 +1,49 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include + +#include +#include + +namespace blob +{ +/** + * load a column of a table (routing, adapter...) from a blob config + **/ +template +::etl::span +loadColumn(::etl::span& column, ::etl::span config) +{ + if (config.size() < sizeof(::etl::be_uint32_t)) + { + column = {}; + return {}; + } + + auto const size = config.take<::etl::be_uint32_t const>(); + if ((config.size() < size) || (size % sizeof(T) != 0)) + { + column = {}; + return {}; + } + + column = config.take(size).reinterpret_as(); + return config; +} + +::blob::Config config(::etl::span blob, ::blob::Config::Type type); + +// [CrcCheck] +bool checkCrc(::etl::span config); +// [CrcCheck] +} // namespace blob diff --git a/libs/bsw/blob/module.spec b/libs/bsw/blob/module.spec new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/bsw/blob/src/blob/Blob.cpp b/libs/bsw/blob/src/blob/Blob.cpp new file mode 100644 index 00000000000..30500a59d79 --- /dev/null +++ b/libs/bsw/blob/src/blob/Blob.cpp @@ -0,0 +1,65 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "blob/Blob.h" + +#include "blob/util.h" + +#include + +namespace blob +{ +// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg): Logger API uses C-style varargs. +::etl::span load(::etl::span const mem) +{ + ::blob::Header header; + if (!::blob::checkHeader(mem, header)) + { + return {}; + } + + size_t const blobSize = Blob::HEADER_SIZE + header.size; + + if (mem.size() < blobSize) + { + logger::Logger::error( + logger::BLOB, + "Invalid blob size (expected=%u,actual=%u)", + static_cast(blobSize), + static_cast(mem.size())); + + return {}; + } + + auto const blob = mem.first(blobSize); + bool hasValidData = true; + size_t i = 0; + for (auto const config : ::blob::Blob(blob)) + { + if (!::blob::checkCrc(config.data)) + { + hasValidData = false; + + logger::Logger::error( + logger::BLOB, "Config %u has invalid data", static_cast(i)); + } + ++i; + } + + if (!hasValidData) + { + return {}; + } + + return blob; +} + +// NOLINTEND(cppcoreguidelines-pro-type-vararg) +} // namespace blob diff --git a/libs/bsw/blob/src/blob/Config.cpp b/libs/bsw/blob/src/blob/Config.cpp new file mode 100644 index 00000000000..6d43fd6fbd6 --- /dev/null +++ b/libs/bsw/blob/src/blob/Config.cpp @@ -0,0 +1,72 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "blob/Config.h" + +#include "blob/Logger.h" + +#include +#include + +namespace blob +{ +// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg): Logger API uses C-style varargs. +Config::Header loadConfigHeader(::etl::span& mem) +{ + if (mem.size() < sizeof(Config::Header)) + { + return {}; + } + + Config::Header header; + header.configType + = static_cast(static_cast(mem.take<::etl::be_uint32_t const>())); + header.reserved = mem.take<::etl::be_uint64_t const>(); + header.size = mem.take<::etl::be_uint32_t const>(); + + return header; +} + +Config Config::from_bytes(::etl::span const mem) +{ + auto data = mem; + + auto const header = loadConfigHeader(data); + + if (header.configType == Type::UNKNOWN) + { + ::util::logger::Logger::error(::util::logger::BLOB, "Unknown config type"); + return {}; + } + + if (header.size > ::etl::numeric_limits::max() - sizeof(header)) + { + ::util::logger::Logger::error( + ::util::logger::BLOB, + "Config header too big: size=%u exceeds maximum", + static_cast(header.size)); + return {}; + } + + auto const size = sizeof(header) + header.size; + if (mem.size() < size) + { + ::util::logger::Logger::error( + ::util::logger::BLOB, + "Invalid config size (expected=%u,actual=%u)", + static_cast(size), + static_cast(mem.size())); + return {}; + } + return {header.configType, mem.first(size)}; +} + +// NOLINTEND(cppcoreguidelines-pro-type-vararg) +} // namespace blob diff --git a/libs/bsw/blob/src/blob/Header.cpp b/libs/bsw/blob/src/blob/Header.cpp new file mode 100644 index 00000000000..092f3241ba5 --- /dev/null +++ b/libs/bsw/blob/src/blob/Header.cpp @@ -0,0 +1,68 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "blob/Header.h" + +#include "blob/Blob.h" +#include "blob/Logger.h" + +#include +#include + +namespace blob +{ +namespace logger = ::util::logger; + +// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg): Logger API uses C-style varargs. + +bool checkHeader(::etl::span const blob, Header& header) +{ + if (blob.size() < Blob::HEADER_SIZE) + { + logger::Logger::error( + logger::BLOB, + "Invalid blob header size (expected=%u,actual=%u)", + static_cast(Blob::HEADER_SIZE), + static_cast(blob.size())); + return false; + } + + auto blobHeader = blob.first(Blob::HEADER_SIZE); + + header.version = blobHeader.take<::etl::be_uint32_t const>(); + if (header.version != Blob::VERSION) + { + logger::Logger::error( + logger::BLOB, + "Invalid blob version (expected=%u,actual=%u)", + Blob::VERSION, + static_cast(header.version)); + return false; + } + + header.magic = blobHeader.take<::etl::be_uint32_t const>(); + + if (header.magic != Blob::MAGIC) + { + logger::Logger::error( + logger::BLOB, + "Invalid blob magic number (expected=0x%x,actual=0x%x)", + Blob::MAGIC, + static_cast(header.magic)); + return false; + } + + header.size = blobHeader.take<::etl::be_uint32_t const>(); + + return true; +} + +// NOLINTEND(cppcoreguidelines-pro-type-vararg) +} // namespace blob diff --git a/libs/bsw/blob/src/blob/Logger.cpp b/libs/bsw/blob/src/blob/Logger.cpp new file mode 100644 index 00000000000..2933c2a733f --- /dev/null +++ b/libs/bsw/blob/src/blob/Logger.cpp @@ -0,0 +1,12 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "blob/Logger.h" +DEFINE_LOGGER_COMPONENT(BLOB) diff --git a/libs/bsw/blob/src/blob/util.cpp b/libs/bsw/blob/src/blob/util.cpp new file mode 100644 index 00000000000..787681c4f39 --- /dev/null +++ b/libs/bsw/blob/src/blob/util.cpp @@ -0,0 +1,49 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "blob/util.h" + +#include "blob/Blob.h" +#include "blob/Config.h" + +#include +#include + +namespace blob +{ +::blob::Config config(::etl::span const blob, ::blob::Config::Type const type) +{ + for (auto const config : ::blob::Blob(blob)) + { + if (config.type != type) + { + continue; + } + return config; + } + return {}; +} + +bool checkCrc(::etl::span config) +{ + constexpr size_t CRC_BYTES_SIZE = 4; + + if (config.size() < CRC_BYTES_SIZE) + { + return false; + } + + auto const configData = config.take(config.size() - CRC_BYTES_SIZE); + uint32_t const actual = config.take<::etl::be_uint32_t const>(); + uint32_t const expected = ::etl::crc32(configData.begin(), configData.end()); + + return expected == actual; +} +} // namespace blob diff --git a/libs/bsw/blob/test/CMakeLists.txt b/libs/bsw/blob/test/CMakeLists.txt new file mode 100644 index 00000000000..3a573c0e4ad --- /dev/null +++ b/libs/bsw/blob/test/CMakeLists.txt @@ -0,0 +1,12 @@ +# How to generate the test blob and its header files: +# python -m blob binary -i routing.jsonl -c blob.routing.table | python -m blob +# header data -n CONFIGURATION_BLOB > include/blob/configuration.h +# python -m blob header config-type > include/blob/ConfigType.h + +target_include_directories(blobConfiguration INTERFACE include) + +add_executable(blobTest src/BlobTest.cpp) + +target_link_libraries(blobTest PRIVATE blob blobConfiguration gmock_main) + +gtest_discover_tests(blobTest PROPERTIES LABELS "blobTest") diff --git a/libs/bsw/blob/test/include/blob/ConfigType.h b/libs/bsw/blob/test/include/blob/ConfigType.h new file mode 100644 index 00000000000..9eafa48d8b3 --- /dev/null +++ b/libs/bsw/blob/test/include/blob/ConfigType.h @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// This is a generated file. Please do not edit it. + +#pragma once + +#include + +namespace blob +{ +enum class ConfigType : uint32_t +{ + ROUTING = 0x00, + RX_ADAPTER = 0x01, + TX_ADAPTER = 0x02, + CHANNEL = 0xCC, + CHANNEL_NAMES = 0xDD, + META = 0xFE, + UNKNOWN = 0xFFFFFFFF +}; +} // namespace blob diff --git a/libs/bsw/blob/test/include/blob/configuration.h b/libs/bsw/blob/test/include/blob/configuration.h new file mode 100644 index 00000000000..616bb197949 --- /dev/null +++ b/libs/bsw/blob/test/include/blob/configuration.h @@ -0,0 +1,155 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// This is a generated file. Please do not edit it. + +#pragma once + +#include + +namespace blob +{ +uint8_t const CONFIGURATION_BLOB[2156] = { + 0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x08, 0x60, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD4, 0x00, 0x00, 0x00, 0x50, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x11, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x17, + 0x00, 0x00, 0x00, 0x5C, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, + 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x01, 0x4D, 0x00, 0x00, 0x02, 0x2B, + 0x00, 0x00, 0x00, 0x6F, 0x00, 0x00, 0x02, 0x9A, 0x00, 0x00, 0x00, 0xDE, 0x00, 0x00, 0x03, 0xE7, + 0x00, 0x00, 0x00, 0x17, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x00, 0x00, 0x00, 0x02, 0x02, + 0x02, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x01, 0x06, 0x02, 0x09, 0xFF, 0x4A, 0x4F, 0xF8, 0x47, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1C, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0xA3, 0x1B, 0x9D, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x11, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x15, + 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1C, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, 0x1A, 0x2E, 0x4E, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x63, 0xB7, 0x34, 0x1F, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC5, 0x50, 0xD0, 0x1F, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, + 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0xBC, 0x00, 0x00, 0x01, 0xBD, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x94, 0x24, 0xD6, 0xE5, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x55, 0x76, 0xA0, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x34, 0x49, 0xC0, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x09, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x3B, 0xFF, 0x83, 0xF4, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x6D, 0xFB, 0x3D, 0x74, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0xC6, 0xAB, 0x3C, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, + 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x8A, 0xD4, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x20, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, + 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x6F, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8B, 0xB8, 0xE0, 0x1A, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x10, 0x03, + 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x00, 0xDE, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x94, 0x74, 0x04, 0xD1, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x4D, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0xDB, 0x79, 0x40, 0x0B, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB6, 0xE8, 0xF3, 0xD5, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x2B, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x53, 0x01, 0x1F, 0x45, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x02, 0x9A, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x0C, 0xCE, 0x05, 0x7C, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xCD, 0xF6, 0x71, 0x36, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x81, 0xE3, 0xFE, 0x18, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0xE7, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x79, 0xAE, 0xAD, 0xFC, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x01, 0x11, 0x00, 0x00, 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, + 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xA8, 0x2B, 0xEE, 0x50, 0x00, 0x00, 0x00, 0xCC, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x01, 0x11, 0x00, 0x00, + 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0x0D, 0x23, 0x85, 0x3C, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x01, 0x11, 0x00, 0x00, 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, + 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x6E, 0x24, 0x5C, 0x18, 0x00, 0x00, 0x00, 0xCC, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x01, 0x11, 0x00, 0x00, + 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0x9C, 0x42, 0x55, 0xA5, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x01, 0x11, 0x00, 0x00, 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, + 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x45, 0x8C, 0x81, 0x00, 0x00, 0x00, 0xCC, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x01, 0x11, 0x00, 0x00, + 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0x5A, 0x4D, 0xE7, 0xED, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x01, 0x11, 0x00, 0x00, 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, + 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x39, 0x4A, 0x3E, 0xC9, 0x00, 0x00, 0x00, 0xCC, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x01, 0x11, 0x00, 0x00, + 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0x65, 0xF0, 0xF2, 0xD6, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x01, 0x11, 0x00, 0x00, 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, + 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x06, 0xF7, 0x2B, 0xF2, 0x00, 0x00, 0x00, 0xDD, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB0, 0x00, 0x0A, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x13, 0x00, 0x22, 0x00, 0x31, 0x00, 0x40, 0x00, 0x4F, 0x00, 0x5E, 0x00, 0x6D, + 0x00, 0x7C, 0x00, 0x92, 0x43, 0x41, 0x4E, 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, + 0x53, 0x50, 0x4F, 0x52, 0x54, 0x30, 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, + 0x50, 0x4F, 0x52, 0x54, 0x31, 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, + 0x4F, 0x52, 0x54, 0x32, 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, + 0x52, 0x54, 0x33, 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, + 0x54, 0x34, 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, + 0x35, 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, 0x36, + 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, 0x37, 0x00, + 0x55, 0x4E, 0x4E, 0x41, 0x4D, 0x45, 0x44, 0x5F, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, + 0x53, 0x50, 0x4F, 0x52, 0x54, 0x00, 0xFF, 0xFF, 0x62, 0x04, 0x60, 0x26}; +} // namespace blob diff --git a/libs/bsw/blob/test/routing.jsonl b/libs/bsw/blob/test/routing.jsonl new file mode 100644 index 00000000000..fa3a5d46d4d --- /dev/null +++ b/libs/bsw/blob/test/routing.jsonl @@ -0,0 +1,29 @@ +{"type":"channel","value":{"name":"CAN","id":0,"type":"can","configuration":{}}} +{"type":"channel","value":{"name":"PDU_TRANSPORT0","id":1,"type":"pdu_transport","configuration":{"connection-type":"multicast","ip-address":"239.192.255.253","local-port":4446,"remote-port":4446, "pcp": 0}}} +{"type":"channel","value":{"name":"PDU_TRANSPORT1","id":2,"type":"pdu_transport","configuration":{"connection-type":"multicast","ip-address":"239.192.255.253","local-port":4446,"remote-port":4446, "pcp": 0}}} +{"type":"channel","value":{"name":"PDU_TRANSPORT2","id":3,"type":"pdu_transport","configuration":{"connection-type":"multicast","ip-address":"239.192.255.253","local-port":4446,"remote-port":4446, "pcp": 0}}} +{"type":"channel","value":{"name":"PDU_TRANSPORT3","id":4,"type":"pdu_transport","configuration":{"connection-type":"multicast","ip-address":"239.192.255.253","local-port":4446,"remote-port":4446, "pcp": 0}}} +{"type":"channel","value":{"name":"PDU_TRANSPORT4","id":5,"type":"pdu_transport","configuration":{"connection-type":"multicast","ip-address":"239.192.255.253","local-port":4446,"remote-port":4446, "pcp": 0}}} +{"type":"channel","value":{"name":"PDU_TRANSPORT5","id":6,"type":"pdu_transport","configuration":{"connection-type":"multicast","ip-address":"239.192.255.253","local-port":4446,"remote-port":4446, "pcp": 0}}} +{"type":"channel","value":{"name":"PDU_TRANSPORT6","id":7,"type":"pdu_transport","configuration":{"connection-type":"multicast","ip-address":"239.192.255.253","local-port":4446,"remote-port":4446, "pcp": 0}}} +{"type":"channel","value":{"name":"PDU_TRANSPORT7","id":8,"type":"pdu_transport","configuration":{"connection-type":"multicast","ip-address":"239.192.255.253","local-port":4446,"remote-port":4446, "pcp": 0}}} +{"type":"channel","value":{"name":"UNNAMED_PDU_TRANSPORT","id":9,"type":"pdu_transport","configuration":{"connection-type":"multicast","ip-address":"239.192.255.253","local-port":4446,"remote-port":4446, "pcp": 0}}} +{"type":"routing","value":{"input":{"channel-name":"CAN","message-id":1,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":256,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN","message-id":2,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":257,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN","message-id":2,"offset":0,"length":4},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":257,"offset":0,"length":4}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN","message-id":2,"offset":4,"length":4},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":257,"offset":4,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN","message-id":3,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":258,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN","message-id":4,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT1","message-id":16384,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN","message-id":257,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":2,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":17,"offset":0,"length":8},"outputs":[{"channel-name":"CAN","message-id":512,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":18,"offset":0,"length":8},"outputs":[{"channel-name":"CAN","message-id":1,"offset":0,"length":8},{"channel-name":"PDU_TRANSPORT1","message-id":4096,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":19,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT1","message-id":4098,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":20,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT1","message-id":4099,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":21,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT1","message-id":4100,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":257,"offset":0,"length":8},"outputs":[{"channel-name":"CAN","message-id":2,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":2,"offset":0,"length":8},"outputs":[{"channel-name":"CAN","message-id":257,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT1","message-id":20480,"offset":0,"length":8},"outputs":[{"channel-name":"CAN","message-id":5,"offset":0,"length":8},{"channel-name":"PDU_TRANSPORT0","message-id":22,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT3","message-id":444,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT2","message-id":333,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT3","message-id":445,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT4","message-id":555,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT6","message-id":777,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":111,"offset":0,"length":8},{"channel-name":"PDU_TRANSPORT5","message-id":666,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT7","message-id":888,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT1","message-id":222,"offset":0,"length":8},{"channel-name":"UNNAMED_PDU_TRANSPORT","message-id":999,"offset":0,"length":8}]}} \ No newline at end of file diff --git a/libs/bsw/blob/test/src/BlobTest.cpp b/libs/bsw/blob/test/src/BlobTest.cpp new file mode 100644 index 00000000000..9159afc9b9c --- /dev/null +++ b/libs/bsw/blob/test/src/BlobTest.cpp @@ -0,0 +1,511 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "blob/Blob.h" + +#include "blob/Header.h" +#include "blob/configuration.h" +#include "blob/util.h" + +#include + +#include +#include + +#include + +#include + +namespace +{ +using namespace ::testing; + +struct TestTable +{ + ::etl::span first; + + ::etl::span<::etl::be_uint16_t const> second; + + ::etl::span<::etl::be_uint32_t const> third; +}; + +/** + * \desc + * Loading columns of a table from a config yields expected data + */ +TEST(BlobTest, load_columns_from_test_config) +{ + uint8_t const data[] + = {0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, + 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, + 0x00, 0x00, 0x00, 0x04, 0x19, 0x32, 0x64, 0xC8, 0x00, 0x00, 0x00, 0x0A, 0x01, 0x77, + 0x02, 0xEE, 0x3A, 0x98, 0x75, 0x30, 0xEA, 0x60, 0x00, 0x00, 0x00, 0x18, 0x07, 0x73, + 0x59, 0x40, 0x0E, 0xE6, 0xB2, 0x80, 0x1D, 0xCD, 0x65, 0x00, 0x3B, 0x9A, 0xCA, 0x00, + 0x77, 0x35, 0x94, 0x00, 0xE6, 0xB2, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}; + + auto config = ::blob::config(data, ::blob::Config::Type::META); + + auto const header = ::blob::loadConfigHeader(config.data); + EXPECT_EQ(header.configType, ::blob::Config::Type::META); + + TestTable table; + + config.data = ::blob::loadColumn(table.first, config.data); + config.data = ::blob::loadColumn(table.second, config.data); + config.data = ::blob::loadColumn(table.third, config.data); + + ::etl::array const expectedFirstColumn = {25, 50, 100, 200}; + ::etl::array const expectedSecondColumn + = {::etl::be_uint16_t(0x177), + ::etl::be_uint16_t(0x2EE), + ::etl::be_uint16_t(0x3A98), + ::etl::be_uint16_t(0x7530), + ::etl::be_uint16_t(0xEA60)}; + ::etl::array const expectedThirdColumn + = {::etl::be_uint32_t(0x7735940), + ::etl::be_uint32_t(0xEE6B280), + ::etl::be_uint32_t(0x1DCD6500), + ::etl::be_uint32_t(0x3B9ACA00), + ::etl::be_uint32_t(0x77359400), + ::etl::be_uint32_t(0xE6B28000)}; + + EXPECT_THAT(table.first, ElementsAreArray(expectedFirstColumn)); + EXPECT_THAT(table.second, ElementsAreArray(expectedSecondColumn)); + EXPECT_THAT(table.third, ElementsAreArray(expectedThirdColumn)); +} + +/** + * \desc + * Checking header of empty blob returns false + */ +TEST(BlobTest, check_header_of_empty_blob) +{ + ::etl::span emptyBlob; + + blob::Header header; + + EXPECT_FALSE(::blob::checkHeader(emptyBlob, header)); +} + +/** + * \desc + * Checking blob with valid header returns true and sets blob's version, magic number, and size + */ +TEST(BlobTest, check_valid_header) +{ + blob::Header header; + + EXPECT_TRUE(::blob::checkHeader(::blob::CONFIGURATION_BLOB, header)); + + EXPECT_EQ(header.version, ::blob::Blob::VERSION); + EXPECT_EQ(header.magic, ::blob::Blob::MAGIC); + EXPECT_EQ(header.size, sizeof(::blob::CONFIGURATION_BLOB) - ::blob::Blob::HEADER_SIZE); +} + +/** + * \desc + * Checking blob with invalid version returns false + */ +TEST(BlobTest, check_invalid_version) +{ + uint8_t data[sizeof(::blob::CONFIGURATION_BLOB)]; + ::etl::copy(::etl::make_span(::blob::CONFIGURATION_BLOB), ::etl::make_span(data)); + + ::etl::span blobHeader = ::etl::make_span(data); + blobHeader.take<::etl::be_uint32_t>() = 0U; // version + + blob::Header header; + + EXPECT_FALSE(::blob::checkHeader(data, header)); +} + +/** + * \desc + * Checking blob with invalid magic number returns false + */ +TEST(BlobTest, check_invalid_magic) +{ + uint8_t data[sizeof(::blob::CONFIGURATION_BLOB)]; + ::etl::copy(::etl::make_span(::blob::CONFIGURATION_BLOB), ::etl::make_span(data)); + + ::etl::span blobHeader = ::etl::make_span(data); + (void)blobHeader.take<::etl::be_uint32_t>(); // version + blobHeader.take<::etl::be_uint32_t>() = 0U; // magic + + blob::Header header; + + EXPECT_FALSE(::blob::checkHeader(data, header)); +} + +/** + * \desc + * Extracting a config from blob yields expected config + */ +TEST(BlobTest, routing_config) +{ + auto const config = ::blob::config(::blob::CONFIGURATION_BLOB, ::blob::Config::Type::ROUTING); + EXPECT_EQ(config.type, ::blob::Config::Type::ROUTING); +} + +/** + * \desc + * Iterating over blob with valid configs yields expected configs + */ +TEST(BlobTest, configs) +{ + ::blob::Config::Type const configTypes[] = { + ::blob::Config::Type::ROUTING, ::blob::Config::Type::RX_ADAPTER, + ::blob::Config::Type::RX_ADAPTER, ::blob::Config::Type::RX_ADAPTER, + ::blob::Config::Type::RX_ADAPTER, ::blob::Config::Type::RX_ADAPTER, + ::blob::Config::Type::RX_ADAPTER, ::blob::Config::Type::RX_ADAPTER, + ::blob::Config::Type::RX_ADAPTER, ::blob::Config::Type::RX_ADAPTER, + ::blob::Config::Type::RX_ADAPTER, ::blob::Config::Type::TX_ADAPTER, + ::blob::Config::Type::TX_ADAPTER, ::blob::Config::Type::TX_ADAPTER, + ::blob::Config::Type::TX_ADAPTER, ::blob::Config::Type::TX_ADAPTER, + ::blob::Config::Type::TX_ADAPTER, ::blob::Config::Type::TX_ADAPTER, + ::blob::Config::Type::TX_ADAPTER, ::blob::Config::Type::TX_ADAPTER, + ::blob::Config::Type::TX_ADAPTER, ::blob::Config::Type::CHANNEL, + ::blob::Config::Type::CHANNEL, ::blob::Config::Type::CHANNEL, + ::blob::Config::Type::CHANNEL, ::blob::Config::Type::CHANNEL, + ::blob::Config::Type::CHANNEL, ::blob::Config::Type::CHANNEL, + ::blob::Config::Type::CHANNEL, ::blob::Config::Type::CHANNEL, + ::blob::Config::Type::CHANNEL_NAMES, + }; + + size_t i = 0; + for (auto config : ::blob::Blob(::blob::CONFIGURATION_BLOB)) + { + EXPECT_EQ(config.type, configTypes[i]) << "i=" << i; + ++i; + } +} + +/** + * \desc + * Iterating over blob with unknown config yields no configs + */ +TEST(BlobTest, unknown_config) +{ + uint8_t const data[] + = {0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x38, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC9, 0xF2, 0xDE, 0x11}; + + for (auto config : ::blob::Blob(data)) + { + EXPECT_EQ(config.data.size(), 0); + } +} + +/** + * \desc + * Iterating over blob with very large config header size yields no configs + */ +TEST(BlobTest, large_config_header_size) +{ + uint8_t const data[] + = {0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC9, 0xF2, 0xDE, 0x11}; + + for (auto config : ::blob::Blob(data)) + { + EXPECT_EQ(config.data.size(), 0); + } +} + +/** + * \desc + * Iterating over blob with invalid size yields no configs + */ +TEST(BlobTest, invalid_size_blob) +{ + uint8_t data[sizeof(::blob::CONFIGURATION_BLOB)]; + ::etl::copy(::etl::make_span(::blob::CONFIGURATION_BLOB), ::etl::make_span(data)); + + ::etl::span blobHeader = ::etl::make_span(data); + (void)blobHeader.take<::etl::be_uint32_t>(); // version + (void)blobHeader.take<::etl::be_uint32_t>(); // magic + blobHeader.take<::etl::be_uint32_t>() = 0U; // size + + for (auto config : ::blob::Blob(data)) + { + FAIL(); // there should be no configs + EXPECT_EQ(config.data.size(), 0); + } +} + +/** + * \desc + * Iterating over empty blob yields no configs + */ +TEST(BlobTest, empty_blob) +{ + ::etl::span emptyData; + + for (auto config : ::blob::Blob(emptyData)) + { + FAIL(); // there should be no configs + EXPECT_EQ(config.data.size(), 0); + } +} + +/** + * \desc + * Checking arrow operator's usage with Blob Iterator + */ +TEST(BlobTest, check_arrow_operator_blob) +{ + constexpr uint32_t FIRST_CONFIG_SIZE = 20; + constexpr uint32_t SECOND_CONFIG_SIZE = 36; + + uint8_t const data[] + = {0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x8B, 0x31, 0x3D, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC9, 0xF2, 0xDE, 0x11}; + + auto blob_it = ::blob::Blob(data).begin(); + + EXPECT_EQ(blob_it->data.size(), FIRST_CONFIG_SIZE); + + ++blob_it; + + EXPECT_EQ(blob_it->data.size(), SECOND_CONFIG_SIZE); +} + +/** + * \desc + * Checking equality operator's usage between Blob Iterators + */ +TEST(BlobTest, check_equality_operator_blob) +{ + uint8_t const data[32] = {0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x8B, 0x31, 0x3D, 0x45}; + + uint8_t other_data[32] = {}; + ::etl::copy(::etl::make_span(data), ::etl::make_span(other_data)); + + auto first_blob_it = ::blob::Blob(data).begin(); + + auto second_blob_it = ::blob::Blob(other_data).begin(); + + // same sizes and same memory locations + EXPECT_TRUE(first_blob_it == first_blob_it); + + // same sizes and different memory locations + EXPECT_FALSE(first_blob_it == second_blob_it); +} + +/** + * \desc + * Checking arrow operator's usage with Config + */ +TEST(BlobTest, check_arrow_operator_config) +{ + constexpr uint32_t FIRST_CONFIG_SIZE = 20; + constexpr uint32_t SECOND_CONFIG_SIZE = 36; + + uint8_t const data[] + = {0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x8B, 0x31, 0x3D, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC9, 0xF2, 0xDE, 0x11}; + + uint32_t const configSizes[] = {FIRST_CONFIG_SIZE, SECOND_CONFIG_SIZE}; + + size_t i = 0; + for (auto config : ::blob::Blob(data)) + { + EXPECT_EQ(config->data.size(), configSizes[i]); + ++i; + } +} + +/** + * \desc + * Checking equality operator's usage between Configs + */ +TEST(BlobTest, check_equality_operator_config) +{ + uint8_t const data[] + = {0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, + 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x8B, 0x31, + 0x3D, 0x45, 0x00, 0x00, 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x8B, 0x31, 0x3D, 0x45, 0x00, 0x00, 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x01, 0x00, 0x34, 0x55, 0x4E, 0x55, + 0x53, 0x45, 0x44, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}; + + auto blob_it = ::blob::Blob(data).begin(); + + auto first_config = (*blob_it); + + auto second_config = (*++blob_it); + + auto third_config = (*++blob_it); + + auto forth_config = (*++blob_it); + + // same type, same size, same memory locations + EXPECT_TRUE(first_config == first_config); + + // same type, same size, different memory locations + EXPECT_FALSE(first_config == second_config); + + // same type, different size, different memory locations + EXPECT_FALSE(second_config == third_config); + + // different type, same size, different memory locations + EXPECT_FALSE(third_config == forth_config); +} + +/** + * \desc + * Loading from non-empty config returns non-empty span with valid config header + */ +TEST(BlobTest, load_config_header_from_non_empty_span) +{ + uint8_t const data[] + = {0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x8B, 0x31, 0x3D, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC9, 0xF2, 0xDE, 0x11}; + + auto const config = ::blob::config(data, ::blob::Config::Type::ROUTING); + + auto headerData = config.data; + auto const header = ::blob::loadConfigHeader(headerData); + + EXPECT_NE(headerData.size(), 0); + + EXPECT_EQ(header.configType, ::blob::Config::Type::ROUTING); + EXPECT_EQ(header.size, 20); +} + +/** + * \desc + * Loading from empty config returns empty span + */ +TEST(BlobTest, load_config_header_from_empty_span) +{ + ::etl::span emptyConfig; + auto const header = ::blob::loadConfigHeader(emptyConfig); + + EXPECT_EQ(emptyConfig.size(), 0); + EXPECT_EQ(header.configType, ::blob::Config::Type::UNKNOWN); +} + +/** + * \desc + * Checking configs with valid crcs returns trues + */ +TEST(BlobTest, check_valid_crc) +{ + size_t i = 0; + for (auto config : ::blob::Blob(::blob::CONFIGURATION_BLOB)) + { + EXPECT_TRUE(::blob::checkCrc(config.data)) << "Config no. " << i << " has invalid data!"; + ++i; + } +} + +/** + * \desc + * Checking config with invalid crc returns false + */ +TEST(BlobTest, check_invalid_crc) +{ + uint8_t const config[] = {0x00, 0x00, 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x8B, 0x31, 0x3D, 0x46}; + + EXPECT_FALSE(::blob::checkCrc(config)); +} + +/** + * \desc + * Checking crc of config with invalid size returns false + */ +TEST(BlobTest, check_crc_of_invalid_size_config) +{ + ::etl::span emptyConfig; + + EXPECT_FALSE(::blob::checkCrc(emptyConfig)); +} + +/** + * \desc + * Loading blob with valid data returns non-empty blob span + */ +TEST(BlobTest, load_valid_blob) { EXPECT_NE(::blob::load(::blob::CONFIGURATION_BLOB).size(), 0); } + +/** + * \desc + * Loading blob with invalid header returns empty blob span + */ +TEST(BlobTest, load_invalid_header_blob) +{ + uint8_t data[sizeof(::blob::CONFIGURATION_BLOB)]; + ::etl::copy(::etl::make_span(::blob::CONFIGURATION_BLOB), ::etl::make_span(data)); + + { + ::etl::span blobHeader = ::etl::make_span(data); + blobHeader.take<::etl::be_uint32_t>() = 0U; // version + } + + EXPECT_EQ(::blob::load(data).size(), 0); + + ::etl::copy(::etl::make_span(::blob::CONFIGURATION_BLOB), ::etl::make_span(data)); + { + ::etl::span blobHeader = ::etl::make_span(data); + (void)blobHeader.take<::etl::be_uint32_t>(); // version + blobHeader.take<::etl::be_uint32_t>() = 0U; // magic + } + + EXPECT_EQ(::blob::load(data).size(), 0); +} + +/** + * \desc + * Loading blob with invalid data returns empty blob span + */ +TEST(BlobTest, load_invalid_data_blob) +{ + uint8_t const data[] + = {0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x8B, 0x31, 0x3D, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC9, 0xF2, 0xDE, 0x11}; + + EXPECT_EQ(::blob::load(data).size(), 0); +} + +/** + * \desc + * Loading blob from empty data returns empty blob span + */ +TEST(BlobTest, load_empty_data_blob) +{ + ::etl::span emptyData; + + EXPECT_EQ(::blob::load(emptyData).size(), 0); +} + +} // namespace diff --git a/libs/bsw/routing/CMakeLists.txt b/libs/bsw/routing/CMakeLists.txt new file mode 100644 index 00000000000..a74e3ca1068 --- /dev/null +++ b/libs/bsw/routing/CMakeLists.txt @@ -0,0 +1,30 @@ +add_library( + routing + src/routing/pduRouting.cpp + src/routing/RxAdapterTable.cpp + src/routing/TxAdapterTable.cpp + src/routing/Header.cpp + src/routing/LegacyTxAdapter.cpp + src/routing/util.cpp + src/routing/Logger.cpp + src/routing/PduTransportTxAdapter.cpp + src/routing/ChannelNames.cpp + src/routing/PduTransportBufferedWriter.cpp + src/routing/RxAdapter.cpp + src/routing/PduTransportConfig.cpp) + +add_library(routingConfiguration INTERFACE) + +target_include_directories(routing PUBLIC include) + +target_link_libraries( + routing + PUBLIC blob + bsp + cpp2ethernet + etl + io + lwipSocket + platform + routingConfiguration + util) diff --git a/libs/bsw/routing/doc/design.rst b/libs/bsw/routing/doc/design.rst new file mode 100644 index 00000000000..73b8eef500c --- /dev/null +++ b/libs/bsw/routing/doc/design.rst @@ -0,0 +1,56 @@ +.. + ******************************************************************************* + Copyright (c) 2026 BMW AG + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + ******************************************************************************* + +Routing detailed design +======================= + +Each routing message uses an 8-byte header followed by its payload: + +* 4 bytes: message ID (big endian) +* 4 bytes: payload length in bytes (big endian) +* N bytes: payload + +``RxAdapter`` or ``PduTransportRxAdapter`` extracts PDU(s) from a payload according to its +``RxAdapterTable`` and implements the ``IReader`` interface. The ``RxAdapterTable`` determines +which PDUs a payload contains with the message ID and the payload length. For each PDU, +it defines a length and an offset in bytes, i.e., the position of the PDU in the payload. + +.. _rxAdapterTable: + +.. literalinclude:: ../include/routing/RxAdapterTable.h + :start-after: RX_ADAPTER_TABLE_BEGIN + :end-before: RX_ADAPTER_TABLE_END + :language: cpp + +``Router`` reads PDUs from channels using ``IReader`` interfaces and writes them to channels using +``IWriter`` interfaces. Its ``PduRoutingTable`` specifies to which channel(s) a PDU should be +forwarded as well as the outgoing message ID. + +.. _pduRoutingTable: + +.. literalinclude:: ../include/routing/PduRoutingTable.h + :start-after: PDU_ROUTING_TABLE_BEGIN + :end-before: PDU_ROUTING_TABLE_END + :language: c++ + + +``LegacyTxAdapter`` or ``PduTransportTxAdapter`` creates outgoing messages out of PDUs according to its +``TxAdapterTable`` and implements the ``IWriter`` interface. For each message, the ``TxAdapterTable`` +determines the payload length and the PDU offset within it. + +.. _txAdapterTable: + +.. literalinclude:: ../include/routing/TxAdapterTable.h + :start-after: TX_ADAPTER_TABLE_BEGIN + :end-before: TX_ADAPTER_TABLE_END + :language: cpp + + diff --git a/libs/bsw/routing/doc/index.rst b/libs/bsw/routing/doc/index.rst new file mode 100644 index 00000000000..a9305a77212 --- /dev/null +++ b/libs/bsw/routing/doc/index.rst @@ -0,0 +1,67 @@ +.. + ******************************************************************************* + Copyright (c) 2026 BMW AG + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + ******************************************************************************* + +routing - PDU routing across channels +===================================== + +Network routers, on the most basic level, take some address or identification information and use +it to distribute a packet/data to one, multiple or no destinations. +This module defines such a router, trying to get rid of as much meta/context information as +possible for abstraction and optimization reasons. + +What are the most basic bits of information? +An input/output-channel (port), identification (ID) and the data which needs to be forwarded/routed. +Only channel and ID are used for the routing decision, the data must not be interpreted. +No information about the content should be leaked implicitly e.g. by trying to encode this +information into the ID, so as not to break the layer separation. + +To be able to plug data into this generic router, we need a way to give context to this data, and to +connect it with buses: the channel adapter. +An adapter packs and unpacks the data for routing: unpacking the PDUs from the format circulating on +the buses, and packing it back up in the correct format on the other end. + +The module ``routing`` contains this PDU router, configured using a binary description of the +routing table. The PDUs can be routed across CAN, FlexRay or Ethernet. + +.. uml:: + :align: center + + Package routing { + () IReader as ir1 + () IWriter as iw1 + + [Router] + [RxAdapter] + [TxAdapter] + } + + () IReader as ir2 + () IWriter as iw2 + [MemoryQueue] as mq1 + [MemoryQueue] as mq2 + + [Router] -left-( ir1 + [Router] -right-( iw1 + [RxAdapter] -right- ir1 + [TxAdapter] -left- iw1 + [RxAdapter] -down-( ir2 + [TxAdapter] -down-( iw2 + + mq1 -up- iw2 + mq2 -up- ir2 + + +.. toctree:: + :maxdepth: 1 + + interface + integration + design diff --git a/libs/bsw/routing/doc/integration.rst b/libs/bsw/routing/doc/integration.rst new file mode 100644 index 00000000000..2b9d8ae6464 --- /dev/null +++ b/libs/bsw/routing/doc/integration.rst @@ -0,0 +1,174 @@ +.. + ******************************************************************************* + Copyright (c) 2026 BMW AG + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + ******************************************************************************* + +Routing Integration +=================== + +Integration +----------- + +For projects using FlexRay, CAN, and PDU transport, the ``Integration`` class provides +a ready-to-go implementation of the instantiation of the routing components. + +The ``Integration`` class uses the ``blob`` module to access configurations, which can be loaded +from memory using the ``load`` functions. + +Here is the structure of this class, with regard to the ``Routing`` components (note that +``CHANNELS = PDU_TRANSPORT_CHANNELS + CAN_CHANNELS + FLEXRAY_CHANNELS``): + +.. uml:: + :align: center + + namespace io { + interface IWriter {} + interface IReader {} + hide members + } + namespace routing { + class Integration {} + class Router {} + class RxAdapter {} + class TxAdapter {} + + + Integration "1" *-- "1" Router + Integration "1" *-- "CHANNELS" RxAdapter + Integration "1" *-- "CHANNELS" TxAdapter + Integration --> blob.Blob : "uses" + + Router "1" --> "CHANNELS" io.IWriter + Router "1" --> "CHANNELS" io.IReader + + RxAdapter --|> io.IReader + TxAdapter --|> io.IWriter + + hide members + } + namespace blob{ + class Blob + + hide members + } + + +PDU Transport Integration +------------------------- + +The ``PduTransportIntegration`` class takes care of instantiating all the components related to the UDP-based PDU +transport communication, except for the sockets. The sockets are provided at initialization and their type has +to derive from ``AbstractDatagramSocket``. ``PduTransportIntegration`` separates frame creation from +socket transmission: + +* ``PduTransportBufferedWriter`` batches outgoing PDU transport messages into frame-sized queue + entries +* ``checkTransmissionTimeouts()`` evaluates whether buffered frames must be committed to TX queues because + of configured timeouts +* ``sendUdpFrames()`` begins the actual UDP transmission by reading frames from the TX queue and sending + them through sockets + +As a result, data already written to TX queues may still not have been sent out. Data is written to the TX queue when +the next message would no longer fit into the current frame buffer or the configured transmission timeout expires. +The timeout starts with the first committed message in an empty frame buffer, not with allocation. + +The structure of this class is the following: + +.. uml:: + :align: center + + namespace udp { + interface AbstractDatagramSocket {} + hide members + } + namespace io { + class MemoryQueueWriter {} + class MemoryQueueReader {} + + MemoryQueueWriter --> MemoryQueue + MemoryQueueReader --> MemoryQueue + + namespace udp { + class Receiver{} + class Sender{} + } + hide members + } + namespace routing { + class PduTransportIntegration {} + + PduTransportIntegration -r-> blob.Blob : "uses" + PduTransportIntegration -r-> PduTransportConfig : "uses" + + PduTransportIntegration "1" *-- "2*PDU_TRANSPORT_CHANNELS" io.MemoryQueue + PduTransportIntegration "1" *-- "2*PDU_TRANSPORT_CHANNELS" io.MemoryQueueReader + PduTransportIntegration "1" *-- "2*PDU_TRANSPORT_CHANNELS" io.MemoryQueueWriter + PduTransportIntegration "1" *-- "PDU_TRANSPORT_CHANNELS" io.udp.Receiver + PduTransportIntegration "1" *-- "PDU_TRANSPORT_CHANNELS" io.udp.Sender + PduTransportIntegration "1" *-- "2*PDU_TRANSPORT_CHANNELS" udp.AbstractDatagramSocket + + hide members + } + namespace blob{ + class Blob + hide members + } + +If we look at the way these components are interconnected and connected with the ``Integration`` class, we get: + +.. uml:: + :align: center + + component Integration { + () IReader as ir1 + () IWriter as iw1 + + [Router] + [RxAdapter] + [TxAdapter] + } + + component PduTransportIntegration { + () IReader as ir4 + () IWriter as iw4 + + [Receiver] + [MemoryQueue] as mq1 + [MemoryQueue] as mq2 + [Sender] + () AbstractDatagramSocket as isock + () AbstractDatagramSocket as osock + } + + () IReader as ir3 + () IWriter as iw3 + + [UdpSocket] as iudp + [UdpSocket] as oudp + + [Router] -left-( ir1 + [Router] -right-( iw1 + [RxAdapter] -right- ir1 + [TxAdapter] -left- iw1 + [RxAdapter] -down-( ir3 + [TxAdapter] -down-( iw3 + + mq1 -up- ir3 + mq2 -up- iw3 + + mq1 -left- iw4 + mq2 -right- ir4 + [Receiver] -right-( iw4 + [Sender] -left-( ir4 + + [Receiver] -down-( isock + [Sender] -down-( osock + + iudp -up- isock + oudp -up- osock diff --git a/libs/bsw/routing/doc/interface.rst b/libs/bsw/routing/doc/interface.rst new file mode 100644 index 00000000000..a81c8240a10 --- /dev/null +++ b/libs/bsw/routing/doc/interface.rst @@ -0,0 +1,144 @@ +.. + ******************************************************************************* + Copyright (c) 2026 BMW AG + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + ******************************************************************************* + +Routing Interface +================= + +Router +------ + +The Router reads PDUs from readers, and writes them to the appropriate writers. + +It is configured using a :ref:`routing table` which describes where the PDUs should +be routed and with which message ID. This table is described in the detailed design, and can be +produced from a high-level jsonl description using the generation tools. + +Properties +++++++++++ + +Given N the number of known PDUs, and M the number of channels, the memory usage range, in bytes, is +given by the following formula: + +.. math:: + + [4 * (N + 1), 4 * (N + 1) + 5 * M * N] + +where the lower bound corresponds to PDUs with no destinations, and the upper bound +corresponds to every PDU being routed to every channel. + +For example, for 10 known PDUs and 3 channels, the memory usage will be in the range +:math:`[44, 194]` bytes. + +Template Parameters ++++++++++++++++++++ + +.. literalinclude:: ../include/routing/Router.h + :start-after: TPARAMS_BEGIN + :end-before: TPARAMS_END + :language: none + +Public API +++++++++++ + +.. literalinclude:: ../include/routing/Router.h + :start-after: PUBLIC_API_BEGIN + :end-before: PUBLIC_API_END + :language: cpp + :dedent: 4 + +RxAdapter +--------- + +The RxAdapter is an adapter that extracts PDUs from messages in the provided +IReader, exposing them with the IReader interface. +It counts the PDUs and bytes extracted from the messages. + +Its configuration is also achieved using an appropriate :ref:`table`. + +Properties +++++++++++ + +Given N the number of known PDUs, M the number of channels, L the number of messages with +explicit configured lengths, and P the number of known messages, the total memory usage of all +adapters, in bytes, is given by the following formula: + +.. math:: + + 8 * M + 8 * P + 4 * L + 8 * N + +For example, for 10 known PDUs, 3 channels, 5 known messages, and 2 configured message lengths, +the memory usage will be 152 bytes. + +Template Parameters ++++++++++++++++++++ + +.. literalinclude:: ../include/routing/PduTransportRxAdapter.h + :start-after: TPARAMS_BEGIN + :end-before: TPARAMS_END + :language: none + +.. literalinclude:: ../include/routing/LegacyRxAdapter.h + :start-after: TPARAMS_BEGIN + :end-before: TPARAMS_END + :language: none + +Public API +++++++++++ + +.. literalinclude:: ../include/routing/RxAdapter.h + :start-after: PUBLIC_API_BEGIN + :end-before: PUBLIC_API_END + :language: cpp + :dedent: 4 + +.. literalinclude:: ../include/routing/PduTransportRxAdapter.h + :start-after: PUBLIC_API_BEGIN + :end-before: PUBLIC_API_END + :language: cpp + :dedent: 4 + +.. literalinclude:: ../include/routing/LegacyRxAdapter.h + :start-after: PUBLIC_API_BEGIN + :end-before: PUBLIC_API_END + :language: cpp + :dedent: 4 + +PduTransportTxAdapter +--------------------- + +The PduTransportTxAdapter is an adapter that forwards PDUs to an IWriter, exposing an IWriter interface +itself. +It collects statistics about the size and number of PDUs forwarded. + +Public API +++++++++++ +.. literalinclude:: ../include/routing/PduTransportTxAdapter.h + :start-after: PUBLIC_API_BEGIN + :end-before: PUBLIC_API_END + :language: cpp + :dedent: 4 + +LegacyTxAdapter +--------------- + +The LegacyTxAdapter is an adapter that forwards PDUs to an IWriter, exposing an IWriter +interface itself. It is meant to be used for legacy buses, like CAN or FlexRay, where the incoming +PDU needs to be sent in a specific position in an outgoing PDU. + +Its configuration is achieved using an appropriate :ref:`table`. + +Public API +++++++++++ +.. literalinclude:: ../include/routing/LegacyTxAdapter.h + :start-after: PUBLIC_API_BEGIN + :end-before: PUBLIC_API_END + :language: cpp + :dedent: 4 diff --git a/libs/bsw/routing/doc/resources/routing_example.dot b/libs/bsw/routing/doc/resources/routing_example.dot new file mode 100644 index 00000000000..421e730cd72 --- /dev/null +++ b/libs/bsw/routing/doc/resources/routing_example.dot @@ -0,0 +1,384 @@ +digraph Routing { + graph[fontname="Arial" fontsize="10" ranksep=1.0 nodesep=1.0 label="routing table"] +rankdir = "LR"label_inputMessageOffsets [label="inputMessageOffsets"] +inputMessageOffsets [shape=record label="{<0> 0| 0} +|{<1> 1| 24} +|{<2> 2| 26} +|{<3> 3| 28} +|{<4> 4| 30} +|{<5> 5| 32} +|{<6> 6| 34} +|{<7> 7| 36} +|{<8> 8| 38} +|{<9> 9| 40} +|{<10> 10| 42} +|{<11> 11| 44} +|{<12> 12| 46} +|{<13> 13| 48} +|<14> 14"] +label_inputMessageOffsets -> inputMessageOffsets:n +label_inputMessageIds [label="inputMessageIds"] +inputMessageIds [shape=record label="{<0> 0| 2201} +|{<1> 1| 2202} +|{<2> 2| 2203} +|{<3> 3| 2204} +|{<4> 4| 2205} +|{<5> 5| 2206} +|{<6> 6| 2207} +|{<7> 7| 2208} +|{<8> 8| 2209} +|{<9> 9| 2210} +|{<10> 10| 2211} +|{<11> 11| 2212} +|{<12> 12| 2401} +|{<13> 13| 2402} +|{<14> 14| 2403} +|{<15> 15| 2404} +|{<16> 16| 2405} +|{<17> 17| 2406} +|{<18> 18| 2407} +|{<19> 19| 2408} +|{<20> 20| 2409} +|{<21> 21| 2410} +|{<22> 22| 2411} +|{<23> 23| 2412} +|{<24> 24| 101} +|{<25> 25| 301} +|{<26> 26| 102} +|{<27> 27| 302} +|{<28> 28| 103} +|{<29> 29| 303} +|{<30> 30| 104} +|{<31> 31| 304} +|{<32> 32| 105} +|{<33> 33| 305} +|{<34> 34| 106} +|{<35> 35| 306} +|{<36> 36| 107} +|{<37> 37| 307} +|{<38> 38| 108} +|{<39> 39| 308} +|{<40> 40| 109} +|{<41> 41| 309} +|{<42> 42| 110} +|{<43> 43| 310} +|{<44> 44| 111} +|{<45> 45| 311} +|{<46> 46| 112} +|{<47> 47| 312} +|<48> 48"] +label_inputMessageIds -> inputMessageIds:n +label_destinationOffsets [label="destinationOffsets"] +destinationOffsets [shape=record label="{<0> 0| 0} +|{<1> 1| 1} +|{<2> 2| 2} +|{<3> 3| 3} +|{<4> 4| 4} +|{<5> 5| 5} +|{<6> 6| 6} +|{<7> 7| 7} +|{<8> 8| 8} +|{<9> 9| 9} +|{<10> 10| 10} +|{<11> 11| 11} +|{<12> 12| 12} +|{<13> 13| 13} +|{<14> 14| 14} +|{<15> 15| 15} +|{<16> 16| 16} +|{<17> 17| 17} +|{<18> 18| 18} +|{<19> 19| 19} +|{<20> 20| 20} +|{<21> 21| 21} +|{<22> 22| 22} +|{<23> 23| 23} +|{<24> 24| 24} +|{<25> 25| 25} +|{<26> 26| 26} +|{<27> 27| 27} +|{<28> 28| 28} +|{<29> 29| 29} +|{<30> 30| 30} +|{<31> 31| 31} +|{<32> 32| 32} +|{<33> 33| 33} +|{<34> 34| 34} +|{<35> 35| 35} +|{<36> 36| 36} +|{<37> 37| 37} +|{<38> 38| 38} +|{<39> 39| 39} +|{<40> 40| 40} +|{<41> 41| 41} +|{<42> 42| 42} +|{<43> 43| 43} +|{<44> 44| 44} +|{<45> 45| 45} +|{<46> 46| 46} +|{<47> 47| 47} +|{<48> 48| 48} +|<49> 49"] +label_destinationOffsets -> destinationOffsets:n +label_destinations [label="destinations"] +destinations [shape=record label="{<0> 0| 1} +|{<1> 1| 2} +|{<2> 2| 3} +|{<3> 3| 4} +|{<4> 4| 5} +|{<5> 5| 6} +|{<6> 6| 7} +|{<7> 7| 8} +|{<8> 8| 9} +|{<9> 9| 10} +|{<10> 10| 11} +|{<11> 11| 12} +|{<12> 12| 1} +|{<13> 13| 2} +|{<14> 14| 3} +|{<15> 15| 4} +|{<16> 16| 5} +|{<17> 17| 6} +|{<18> 18| 7} +|{<19> 19| 8} +|{<20> 20| 9} +|{<21> 21| 10} +|{<22> 22| 11} +|{<23> 23| 12} +|{<24> 24| 0} +|{<25> 25| 0} +|{<26> 26| 0} +|{<27> 27| 0} +|{<28> 28| 0} +|{<29> 29| 0} +|{<30> 30| 0} +|{<31> 31| 0} +|{<32> 32| 0} +|{<33> 33| 0} +|{<34> 34| 0} +|{<35> 35| 0} +|{<36> 36| 0} +|{<37> 37| 0} +|{<38> 38| 0} +|{<39> 39| 0} +|{<40> 40| 0} +|{<41> 41| 0} +|{<42> 42| 0} +|{<43> 43| 0} +|{<44> 44| 0} +|{<45> 45| 0} +|{<46> 46| 0} +|{<47> 47| 0} +|<48> 48"] +label_destinations -> destinations:n +label_outputMessageIds [label="outputMessageIds"] +outputMessageIds [shape=record label="{<0> 0| 201} +|{<1> 1| 202} +|{<2> 2| 203} +|{<3> 3| 204} +|{<4> 4| 205} +|{<5> 5| 206} +|{<6> 6| 207} +|{<7> 7| 208} +|{<8> 8| 209} +|{<9> 9| 210} +|{<10> 10| 211} +|{<11> 11| 212} +|{<12> 12| 301} +|{<13> 13| 302} +|{<14> 14| 303} +|{<15> 15| 304} +|{<16> 16| 305} +|{<17> 17| 306} +|{<18> 18| 307} +|{<19> 19| 308} +|{<20> 20| 309} +|{<21> 21| 310} +|{<22> 22| 311} +|{<23> 23| 312} +|{<24> 24| 2101} +|{<25> 25| 2301} +|{<26> 26| 2102} +|{<27> 27| 2302} +|{<28> 28| 2103} +|{<29> 29| 2303} +|{<30> 30| 2104} +|{<31> 31| 2304} +|{<32> 32| 2105} +|{<33> 33| 2305} +|{<34> 34| 2106} +|{<35> 35| 2306} +|{<36> 36| 2107} +|{<37> 37| 2307} +|{<38> 38| 2108} +|{<39> 39| 2308} +|{<40> 40| 2109} +|{<41> 41| 2309} +|{<42> 42| 2110} +|{<43> 43| 2310} +|{<44> 44| 2111} +|{<45> 45| 2311} +|{<46> 46| 2112} +|{<47> 47| 2312} +|<48> 48"] +label_outputMessageIds -> outputMessageIds:n +inputMessageOffsets:val0 -> inputMessageIds:0:w +inputMessageOffsets:val1 -> inputMessageIds:24:w +inputMessageOffsets:val2 -> inputMessageIds:26:w +inputMessageOffsets:val3 -> inputMessageIds:28:w +inputMessageOffsets:val4 -> inputMessageIds:30:w +inputMessageOffsets:val5 -> inputMessageIds:32:w +inputMessageOffsets:val6 -> inputMessageIds:34:w +inputMessageOffsets:val7 -> inputMessageIds:36:w +inputMessageOffsets:val8 -> inputMessageIds:38:w +inputMessageOffsets:val9 -> inputMessageIds:40:w +inputMessageOffsets:val10 -> inputMessageIds:42:w +inputMessageOffsets:val11 -> inputMessageIds:44:w +inputMessageOffsets:val12 -> inputMessageIds:46:w +inputMessageOffsets:val13 -> inputMessageIds:48:w +inputMessageIds:val0 -> destinationOffsets:0:w +inputMessageIds:val1 -> destinationOffsets:1:w +inputMessageIds:val2 -> destinationOffsets:2:w +inputMessageIds:val3 -> destinationOffsets:3:w +inputMessageIds:val4 -> destinationOffsets:4:w +inputMessageIds:val5 -> destinationOffsets:5:w +inputMessageIds:val6 -> destinationOffsets:6:w +inputMessageIds:val7 -> destinationOffsets:7:w +inputMessageIds:val8 -> destinationOffsets:8:w +inputMessageIds:val9 -> destinationOffsets:9:w +inputMessageIds:val10 -> destinationOffsets:10:w +inputMessageIds:val11 -> destinationOffsets:11:w +inputMessageIds:val12 -> destinationOffsets:12:w +inputMessageIds:val13 -> destinationOffsets:13:w +inputMessageIds:val14 -> destinationOffsets:14:w +inputMessageIds:val15 -> destinationOffsets:15:w +inputMessageIds:val16 -> destinationOffsets:16:w +inputMessageIds:val17 -> destinationOffsets:17:w +inputMessageIds:val18 -> destinationOffsets:18:w +inputMessageIds:val19 -> destinationOffsets:19:w +inputMessageIds:val20 -> destinationOffsets:20:w +inputMessageIds:val21 -> destinationOffsets:21:w +inputMessageIds:val22 -> destinationOffsets:22:w +inputMessageIds:val23 -> destinationOffsets:23:w +inputMessageIds:val24 -> destinationOffsets:24:w +inputMessageIds:val25 -> destinationOffsets:25:w +inputMessageIds:val26 -> destinationOffsets:26:w +inputMessageIds:val27 -> destinationOffsets:27:w +inputMessageIds:val28 -> destinationOffsets:28:w +inputMessageIds:val29 -> destinationOffsets:29:w +inputMessageIds:val30 -> destinationOffsets:30:w +inputMessageIds:val31 -> destinationOffsets:31:w +inputMessageIds:val32 -> destinationOffsets:32:w +inputMessageIds:val33 -> destinationOffsets:33:w +inputMessageIds:val34 -> destinationOffsets:34:w +inputMessageIds:val35 -> destinationOffsets:35:w +inputMessageIds:val36 -> destinationOffsets:36:w +inputMessageIds:val37 -> destinationOffsets:37:w +inputMessageIds:val38 -> destinationOffsets:38:w +inputMessageIds:val39 -> destinationOffsets:39:w +inputMessageIds:val40 -> destinationOffsets:40:w +inputMessageIds:val41 -> destinationOffsets:41:w +inputMessageIds:val42 -> destinationOffsets:42:w +inputMessageIds:val43 -> destinationOffsets:43:w +inputMessageIds:val44 -> destinationOffsets:44:w +inputMessageIds:val45 -> destinationOffsets:45:w +inputMessageIds:val46 -> destinationOffsets:46:w +inputMessageIds:val47 -> destinationOffsets:47:w +destinationOffsets:val0 -> destinations:0:w +destinationOffsets:val1 -> destinations:1:w +destinationOffsets:val2 -> destinations:2:w +destinationOffsets:val3 -> destinations:3:w +destinationOffsets:val4 -> destinations:4:w +destinationOffsets:val5 -> destinations:5:w +destinationOffsets:val6 -> destinations:6:w +destinationOffsets:val7 -> destinations:7:w +destinationOffsets:val8 -> destinations:8:w +destinationOffsets:val9 -> destinations:9:w +destinationOffsets:val10 -> destinations:10:w +destinationOffsets:val11 -> destinations:11:w +destinationOffsets:val12 -> destinations:12:w +destinationOffsets:val13 -> destinations:13:w +destinationOffsets:val14 -> destinations:14:w +destinationOffsets:val15 -> destinations:15:w +destinationOffsets:val16 -> destinations:16:w +destinationOffsets:val17 -> destinations:17:w +destinationOffsets:val18 -> destinations:18:w +destinationOffsets:val19 -> destinations:19:w +destinationOffsets:val20 -> destinations:20:w +destinationOffsets:val21 -> destinations:21:w +destinationOffsets:val22 -> destinations:22:w +destinationOffsets:val23 -> destinations:23:w +destinationOffsets:val24 -> destinations:24:w +destinationOffsets:val25 -> destinations:25:w +destinationOffsets:val26 -> destinations:26:w +destinationOffsets:val27 -> destinations:27:w +destinationOffsets:val28 -> destinations:28:w +destinationOffsets:val29 -> destinations:29:w +destinationOffsets:val30 -> destinations:30:w +destinationOffsets:val31 -> destinations:31:w +destinationOffsets:val32 -> destinations:32:w +destinationOffsets:val33 -> destinations:33:w +destinationOffsets:val34 -> destinations:34:w +destinationOffsets:val35 -> destinations:35:w +destinationOffsets:val36 -> destinations:36:w +destinationOffsets:val37 -> destinations:37:w +destinationOffsets:val38 -> destinations:38:w +destinationOffsets:val39 -> destinations:39:w +destinationOffsets:val40 -> destinations:40:w +destinationOffsets:val41 -> destinations:41:w +destinationOffsets:val42 -> destinations:42:w +destinationOffsets:val43 -> destinations:43:w +destinationOffsets:val44 -> destinations:44:w +destinationOffsets:val45 -> destinations:45:w +destinationOffsets:val46 -> destinations:46:w +destinationOffsets:val47 -> destinations:47:w +destinationOffsets:val48 -> destinations:48:w +destinations:val0 -> outputMessageIds:0:w +destinations:val1 -> outputMessageIds:1:w +destinations:val2 -> outputMessageIds:2:w +destinations:val3 -> outputMessageIds:3:w +destinations:val4 -> outputMessageIds:4:w +destinations:val5 -> outputMessageIds:5:w +destinations:val6 -> outputMessageIds:6:w +destinations:val7 -> outputMessageIds:7:w +destinations:val8 -> outputMessageIds:8:w +destinations:val9 -> outputMessageIds:9:w +destinations:val10 -> outputMessageIds:10:w +destinations:val11 -> outputMessageIds:11:w +destinations:val12 -> outputMessageIds:12:w +destinations:val13 -> outputMessageIds:13:w +destinations:val14 -> outputMessageIds:14:w +destinations:val15 -> outputMessageIds:15:w +destinations:val16 -> outputMessageIds:16:w +destinations:val17 -> outputMessageIds:17:w +destinations:val18 -> outputMessageIds:18:w +destinations:val19 -> outputMessageIds:19:w +destinations:val20 -> outputMessageIds:20:w +destinations:val21 -> outputMessageIds:21:w +destinations:val22 -> outputMessageIds:22:w +destinations:val23 -> outputMessageIds:23:w +destinations:val24 -> outputMessageIds:24:w +destinations:val25 -> outputMessageIds:25:w +destinations:val26 -> outputMessageIds:26:w +destinations:val27 -> outputMessageIds:27:w +destinations:val28 -> outputMessageIds:28:w +destinations:val29 -> outputMessageIds:29:w +destinations:val30 -> outputMessageIds:30:w +destinations:val31 -> outputMessageIds:31:w +destinations:val32 -> outputMessageIds:32:w +destinations:val33 -> outputMessageIds:33:w +destinations:val34 -> outputMessageIds:34:w +destinations:val35 -> outputMessageIds:35:w +destinations:val36 -> outputMessageIds:36:w +destinations:val37 -> outputMessageIds:37:w +destinations:val38 -> outputMessageIds:38:w +destinations:val39 -> outputMessageIds:39:w +destinations:val40 -> outputMessageIds:40:w +destinations:val41 -> outputMessageIds:41:w +destinations:val42 -> outputMessageIds:42:w +destinations:val43 -> outputMessageIds:43:w +destinations:val44 -> outputMessageIds:44:w +destinations:val45 -> outputMessageIds:45:w +destinations:val46 -> outputMessageIds:46:w +destinations:val47 -> outputMessageIds:47:w +} diff --git a/libs/bsw/routing/include/io/udp/Receiver.h b/libs/bsw/routing/include/io/udp/Receiver.h new file mode 100644 index 00000000000..9a03b3649e6 --- /dev/null +++ b/libs/bsw/routing/include/io/udp/Receiver.h @@ -0,0 +1,126 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace io +{ +namespace udp +{ +namespace logger = ::util::logger; + +class Receiver : private ::udp::IDataListener +{ +public: + Receiver( + ::udp::AbstractDatagramSocket& socket, + ::io::IWriter& output, + ::routing::ErrorHandler const errorHandler, + ::etl::span const remoteIpAddresses = {}) + : _socket(socket) + , _output(output) + , _errorHandler(errorHandler) + , _remoteIpAddresses(remoteIpAddresses) + , _lastAllocationFailed(false) + , _invalidIpAddressPdus() + , _failedMemAllocPdus() + { + _socket.setDataListener(this); + } + + virtual ~Receiver() {} + + ::routing::StatCounter::Type invalidIpAddressPdus() const { return _invalidIpAddressPdus; } + + ::routing::StatCounter::Type failedMemAllocPdus() const { return _failedMemAllocPdus; } + +private: + void dataReceived( + ::udp::AbstractDatagramSocket& /* socket */, + ::ip::IPAddress sourceAddress, + uint16_t /* sourcePort */, + ::ip::IPAddress /* destinationAddress */, + uint16_t length) override + { + if (!_remoteIpAddresses.empty()) + { + auto const sourceIpAddress = ::ip::packed(sourceAddress); + auto remoteIpAddresses = _remoteIpAddresses; + bool isValidAddress = false; + while (!remoteIpAddresses.empty()) + { + auto const remoteIpAddress + = remoteIpAddresses.take(sourceIpAddress.size()); + if (::etl::mem_compare( + sourceIpAddress.begin(), sourceIpAddress.end(), remoteIpAddress.begin()) + == 0) + { + isValidAddress = true; + break; + } + } + + if (!isValidAddress) + { + (void)_socket.read(nullptr, length); + + _errorHandler( + ::routing::ErrorHandler::StatusCode::INVALID_REMOTE_IP_ADDRESS, sourceAddress); + + ++_invalidIpAddressPdus; + + return; + } + } + + auto const frame = _output.allocate(length); + if (frame.size() != length) + { + (void)_socket.read(nullptr, length); + + if (!_lastAllocationFailed) + { + _errorHandler(::routing::ErrorHandler::StatusCode::MEM_ALLOCATION_FAILURE); + _lastAllocationFailed = true; + } + + ++_failedMemAllocPdus; + + return; + } + _lastAllocationFailed = false; + + (void)_socket.read(frame.data(), length); + + _output.commit(); + } + + ::udp::AbstractDatagramSocket& _socket; + ::io::IWriter& _output; + ::routing::ErrorHandler const _errorHandler; + ::etl::span const _remoteIpAddresses; + bool _lastAllocationFailed; + + mutable ::routing::StatCounter _invalidIpAddressPdus; + mutable ::routing::StatCounter _failedMemAllocPdus; +}; + +} // namespace udp +} // namespace io diff --git a/libs/bsw/routing/include/io/udp/Sender.h b/libs/bsw/routing/include/io/udp/Sender.h new file mode 100644 index 00000000000..dac71b52649 --- /dev/null +++ b/libs/bsw/routing/include/io/udp/Sender.h @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include +#include +#include + +#include + +namespace io +{ +namespace udp +{ +class Sender +{ +public: + Sender(::io::IReader& input, ::udp::AbstractDatagramSocket& socket) + : _input(input), _socket(socket), _socketErrorPdus() + {} + + void run(size_t const maxNumFrames) { send(maxNumFrames); } + + ::routing::StatCounter::Type socketErrorPdus() const { return _socketErrorPdus; } + +private: + void send(size_t const maxNumFrames) + { + size_t count = 0U; + auto data = _input.peek(); + while ((count < maxNumFrames) && (!data.empty())) + { + if (_socket.send(data) != ::udp::AbstractDatagramSocket::ErrorCode::UDP_SOCKET_OK) + { + ++_socketErrorPdus; + } + _input.release(); + count++; + data = _input.peek(); + } + } + + ::io::IReader& _input; + ::udp::AbstractDatagramSocket& _socket; + + mutable ::routing::StatCounter _socketErrorPdus; +}; + +} // namespace udp +} // namespace io diff --git a/libs/bsw/routing/include/routing/ChannelNames.h b/libs/bsw/routing/include/routing/ChannelNames.h new file mode 100644 index 00000000000..16e484555f2 --- /dev/null +++ b/libs/bsw/routing/include/routing/ChannelNames.h @@ -0,0 +1,39 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include +#include + +#include + +namespace routing +{ + +struct ChannelNames +{ + ::etl::span<::etl::be_uint16_t const> offsets; + ::etl::span names; +}; + +/** + * Returns the name of the channel with the given ID, or an empty span if not found. + */ +::etl::span name(::routing::ChannelNames const& config, uint8_t channelId); + +/** + * Loads channel names from mem. + * Returns true on success and false on error. + * On failure, channel names may be partially modified and must not be used. + */ +bool load(::etl::span mem, ChannelNames& channelNames); + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/ClampedCounter.h b/libs/bsw/routing/include/routing/ClampedCounter.h new file mode 100644 index 00000000000..2272849739d --- /dev/null +++ b/libs/bsw/routing/include/routing/ClampedCounter.h @@ -0,0 +1,59 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include +#include + +#include + +namespace routing +{ +template +struct ClampedCounter +{ + static_assert( + ::etl::is_unsigned::value && ::etl::is_integral::value, + "Value is not an unsigned integral type"); + + using Type = T; + + ClampedCounter() = default; + + operator T() const { return count; } + + T& operator++() { return count < ::etl::numeric_limits::max() ? ++count : count; } + + T operator++(int) + { + T const tmp = count; + (void)this->operator++(); + return tmp; + } + + T& operator+=(T const& n) + { + T const res = count + n; + if (count > res) + { + count = ::etl::numeric_limits::max(); + } + else + { + count = res; + } + return count; + } + + T count = 0; +}; + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/ErrorHandler.h b/libs/bsw/routing/include/routing/ErrorHandler.h new file mode 100644 index 00000000000..58a2b972c85 --- /dev/null +++ b/libs/bsw/routing/include/routing/ErrorHandler.h @@ -0,0 +1,76 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/Header.h" + +#include +#include +#include + +namespace routing +{ +struct ErrorHandler +{ + enum class StatusCode : uint8_t + { + OK, + // There is not enough data to parse the header. + INVALID_MESSAGE_HEADER_SIZE, + // The message ID isn't found in the RX/TX adapter table. + UNKNOWN_MESSAGE_ID, + // The parsed length of the message is larger than the length of data available. + INVALID_PARSED_LENGTH, + // The parsed length of the incoming message doesn't match the configured one (for messages + // which have a configured messaged length). + INVALID_MESSAGE_LENGTH, + // The size of every PDU expected for the parsed ID is larger than the length of data + // available. Only if at least a PDU is expected for this ID. + INVALID_PAYLOAD_LENGTH, + // There is not enough free space in the queue to write the outgoing PDU. + MEM_ALLOCATION_FAILURE, + // A PDU transport channel received data from an invalid source. + INVALID_REMOTE_IP_ADDRESS, + }; + + using Function = ::etl::delegate; + + ErrorHandler() = default; + + ErrorHandler(Function const f, uint8_t const channelId) : _f(f), _channelId(channelId) {} + + void operator()(StatusCode const statusCode) const + { + uint32_t const messageId = 0U; + _f(statusCode, _channelId, messageId); + } + + void operator()(StatusCode const statusCode, ::etl::span message) const + { + uint32_t const messageId + = (statusCode != StatusCode::INVALID_MESSAGE_HEADER_SIZE) + && (message.size() >= sizeof(::etl::be_uint32_t)) + ? static_cast(message.take<::etl::be_uint32_t const>()) + : 0U; + _f(statusCode, _channelId, messageId); + } + + void operator()(StatusCode const statusCode, ::ip::IPAddress const& sourceIpAddress) const + { + _f(statusCode, _channelId, ::ip::ip4_to_u32(sourceIpAddress)); + } + +private: + Function _f; + uint8_t const _channelId = ::routing::INVALID_CHANNEL_ID; +}; + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/Header.h b/libs/bsw/routing/include/routing/Header.h new file mode 100644 index 00000000000..9de50ee8d5c --- /dev/null +++ b/libs/bsw/routing/include/routing/Header.h @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +/** + * This file defines and provides functions to interact with the header common to all routing + * configuration blobs. + */ + +#include +#include +#include + +#include + +namespace routing +{ +static constexpr auto INVALID_CHANNEL_ID = ::etl::numeric_limits::max(); + +enum class ChannelType : uint32_t +{ + PDU_TRANSPORT = 0x00, + CAN = 0x01, + FR = 0x02, + UNKNOWN = 0x03 +}; + +struct Header +{ + ::blob::ConfigType configType = ::blob::ConfigType::UNKNOWN; + ::routing::ChannelType channelType = ::routing::ChannelType::UNKNOWN; + uint8_t channelId = INVALID_CHANNEL_ID; +}; + +/** + * Loads the header from mem, adavancing mem past the header. + * Returns true on success and false on error. + * On failure, the header may be partially modified and must not be used. + */ +bool load(::etl::span& mem, Header& header); + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/Integration.h b/libs/bsw/routing/include/routing/Integration.h new file mode 100644 index 00000000000..acef2f8d83b --- /dev/null +++ b/libs/bsw/routing/include/routing/Integration.h @@ -0,0 +1,510 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "io/udp/Receiver.h" +#include "io/udp/Sender.h" +#include "routing/ChannelNames.h" +#include "routing/ErrorHandler.h" +#include "routing/Header.h" +#include "routing/LegacyRxAdapter.h" +#include "routing/LegacyTxAdapter.h" +#include "routing/Logger.h" +#include "routing/PduTransportConfig.h" +#include "routing/PduTransportRxAdapter.h" +#include "routing/PduTransportTxAdapter.h" +#include "routing/Router.h" +#include "routing/RxAdapter.h" +#include "routing/RxAdapterTable.h" +#include "routing/TxAdapterTable.h" +#include "routing/util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace routing +{ +namespace logger = ::util::logger; + +template< + uint8_t MAX_NUM_PDU_TRANSPORT_CHANNELS, + uint8_t NUM_CAN_CHANNELS, + uint8_t NUM_FLEXRAY_CHANNELS, + size_t MAX_PDU_TRANSPORT_CHANNEL_ELEMENT_SIZE, + size_t MAX_CAN_CHANNEL_ELEMENT_SIZE, + size_t MAX_FLEXRAY_CHANNEL_ELEMENT_SIZE> +class Integration +{ +public: + static_assert( + MAX_NUM_PDU_TRANSPORT_CHANNELS + NUM_CAN_CHANNELS + NUM_FLEXRAY_CHANNELS > 0, + "Number of channels must be greater than 0"); + + static constexpr uint8_t MAX_NUM_CHANNELS + = MAX_NUM_PDU_TRANSPORT_CHANNELS + NUM_CAN_CHANNELS + NUM_FLEXRAY_CHANNELS; + static constexpr uint8_t FIRST_PDU_TRANSPORT_CHANNEL_ID + = MAX_NUM_CHANNELS - MAX_NUM_PDU_TRANSPORT_CHANNELS; + + template + struct ChannelAdapter + { + uint8_t channelId = INVALID_CHANNEL_ID; + RxAdapterType* rxAdapter = nullptr; + TxAdapterType* txAdapter = nullptr; + }; + + using PduTransportRxAdapter + = ::routing::PduTransportRxAdapter; + using CanRxAdapter = ::routing::LegacyRxAdapter; + using FlexrayRxAdapter = ::routing::LegacyRxAdapter; + + using PduTransportChannelAdapter = ChannelAdapter; + using CanChannelAdapter = ChannelAdapter; + using FlexrayChannelAdapter = ChannelAdapter; + + Integration() + : _initialized(false) + , _numberOfPduTransportChannels(0U) + , _numberOfCanChannels(0U) + , _numberOfFlexrayChannels(0U) + , _pduTransportRxAdapters() + , _pduTransportTxAdapters() + , _pduTransportChannelAdapters() + , _canRxAdapters() + , _canTxAdapters() + , _canChannelAdapters() + , _flexrayRxAdapters() + , _flexrayTxAdapters() + , _flexrayChannelAdapters() + , _readers() + , _writers() + , _router() + { + _readers.fill(nullptr); + _writers.fill(nullptr); + } + + void init( + ::etl::span const blobData, + ::etl::span<::io::IReader*> const pduTransportReaders, + ::etl::span<::io::IWriter*> const pduTransportWriters, + ::etl::span const canChannelIds, + ::etl::span<::io::IReader*, NUM_CAN_CHANNELS> const canReaders, + ::etl::span<::io::IWriter*, NUM_CAN_CHANNELS> const canWriters, + ::etl::span const frChannelIds, + ::etl::span<::io::IReader*, NUM_FLEXRAY_CHANNELS> const flexrayReaders, + ::etl::span<::io::IWriter*, NUM_FLEXRAY_CHANNELS> const flexrayWriters, + ::routing::ErrorHandler::Function const errorHandlingFunction) + { + if (_initialized) + { + return; + } + + if (blobData.empty()) + { + logger::Logger::error( + logger::ROUTING, "Failed to initialize integration due to empty blob"); + return; + } + + size_t const maxNumberOfPduTransportChannels = ::etl::min( + static_cast(MAX_NUM_PDU_TRANSPORT_CHANNELS), + ::etl::min(pduTransportReaders.size(), pduTransportWriters.size())); + size_t const numberOfChannels + = maxNumberOfPduTransportChannels + NUM_CAN_CHANNELS + NUM_FLEXRAY_CHANNELS; + + auto const readers = ::etl::make_span(_readers).first(numberOfChannels); + auto const writers = ::etl::make_span(_writers).first(numberOfChannels); + + auto const routingTable = ::blob::config(blobData, ::blob::Config::Type::ROUTING).data; + ::routing::ChannelNames channelNamesConfig; + (void)::routing::load( + ::routing::config(blobData, ::blob::Config::Type::CHANNEL_NAMES).data, + channelNamesConfig); + + for (auto const config : ::blob::Blob(blobData)) + { + if (config.type == ::blob::Config::Type::RX_ADAPTER) + { + auto data = config.data; + Header header; + if (!load(data, header)) + { + continue; + } + + auto const channelId = header.channelId; + + if ((channelId >= numberOfChannels) || (channelId == INVALID_CHANNEL_ID)) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to add channel with invalid ID %u", + static_cast(channelId)); + continue; + } + + logger::Logger::debug( + logger::ROUTING, + "Adding channel ID %u of type %x", + static_cast(channelId), + static_cast(header.channelType)); + + auto const channelName + = ::routing::name(channelNamesConfig, channelId).reinterpret_as(); + auto const channelNameStr + = channelName.empty() || (channelName.back() != '\0') ? "" : channelName.data(); + + switch (header.channelType) + { + case ChannelType::CAN: + { + if (_numberOfCanChannels >= NUM_CAN_CHANNELS) + { + continue; + } + + auto const txConfig = ::routing::config( + blobData, ::blob::Config::Type::TX_ADAPTER, channelId); + if (txConfig.type == ::blob::ConfigType::UNKNOWN) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to find TX adapter config for %u", + static_cast(channelId)); + continue; + } + + ::routing::TxAdapterTable txAdapterTable; + if (!::routing::load(txConfig.data, txAdapterTable)) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to load TX adapter table for channel %u", + static_cast(channelId)); + continue; + } + + auto const it + = etl::find(canChannelIds.begin(), canChannelIds.end(), channelId); + if (it == canChannelIds.end()) + { + logger::Logger::warn( + logger::ROUTING, + "Channel ID %u not found while adding CAN channel", + static_cast(channelId)); + continue; + } + + auto const index + = static_cast(::etl::distance(canChannelIds.begin(), it)); + + if ((canReaders[index] == nullptr) || (canWriters[index] == nullptr)) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to add CAN channel (ID=%u,name=%s) due to invalid reader " + "or writer", + static_cast(channelId), + channelNameStr); + continue; + } + + ::routing::RxAdapterTable rxAdapterTable; + if (!::routing::load(config.data, rxAdapterTable)) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to load RX adapter table for channel %u", + static_cast(channelId)); + continue; + } + + logger::Logger::debug( + logger::ROUTING, + "Adding CAN channel (ID=%u,name=%s) to router", + static_cast(channelId), + channelNameStr); + + auto reader = &_canRxAdapters[index].emplace( + *canReaders[index], + rxAdapterTable, + ::routing::ErrorHandler(errorHandlingFunction, channelId)); + auto writer = &_canTxAdapters[index].emplace( + *canWriters[index], + txAdapterTable, + ::routing::ErrorHandler(errorHandlingFunction, channelId)); + + auto& canChannelAdapter = _canChannelAdapters[_numberOfCanChannels]; + canChannelAdapter.channelId = channelId; + canChannelAdapter.rxAdapter = reader; + canChannelAdapter.txAdapter = writer; + + readers[channelId] = reader; + writers[channelId] = writer; + + ++_numberOfCanChannels; + } + break; + case ChannelType::PDU_TRANSPORT: + { + if (_numberOfPduTransportChannels >= maxNumberOfPduTransportChannels) + { + continue; + } + + if (channelId < FIRST_PDU_TRANSPORT_CHANNEL_ID) + { + logger::Logger::warn( + logger::ROUTING, + "Invalid PDU TP channel ID %u", + static_cast(channelId)); + continue; + } + + uint8_t const index = channelId - FIRST_PDU_TRANSPORT_CHANNEL_ID; + + if ((pduTransportReaders[index] == nullptr) + || (pduTransportWriters[index] == nullptr)) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to add PDU TP channel (ID=%u,name=%s) due to " + "invalid reader " + "or writer", + static_cast(channelId), + channelNameStr); + continue; + } + + ::routing::RxAdapterTable rxAdapterTable; + if (!::routing::load(config.data, rxAdapterTable)) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to load RX adapter table for channel %u", + static_cast(channelId)); + continue; + } + + logger::Logger::debug( + logger::ROUTING, + "Adding PDU TP channel (ID=%u,name=%s) to router", + static_cast(channelId), + channelNameStr); + + auto reader = &_pduTransportRxAdapters[index].emplace( + *pduTransportReaders[index], + rxAdapterTable, + ::routing::ErrorHandler(errorHandlingFunction, channelId)); + auto writer = &_pduTransportTxAdapters[index].emplace( + *pduTransportWriters[index], + ::routing::ErrorHandler(errorHandlingFunction, channelId)); + + auto& pduTransportChannelAdapter + = _pduTransportChannelAdapters[_numberOfPduTransportChannels]; + pduTransportChannelAdapter.channelId = channelId; + pduTransportChannelAdapter.rxAdapter = reader; + pduTransportChannelAdapter.txAdapter = writer; + + readers[channelId] = reader; + writers[channelId] = writer; + + ++_numberOfPduTransportChannels; + } + break; + case ChannelType::FR: + { + if (_numberOfFlexrayChannels >= NUM_FLEXRAY_CHANNELS) + { + continue; + } + + auto const txConfig = ::routing::config( + blobData, ::blob::Config::Type::TX_ADAPTER, channelId); + if (txConfig.type == ::blob::ConfigType::UNKNOWN) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to find TX adapter config for %u", + static_cast(channelId)); + continue; + } + + ::routing::TxAdapterTable txAdapterTable; + if (!::routing::load(txConfig.data, txAdapterTable)) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to load TX adapter table for channel %u", + static_cast(channelId)); + continue; + } + + auto const it + = etl::find(frChannelIds.begin(), frChannelIds.end(), channelId); + if (it == frChannelIds.end()) + { + logger::Logger::warn( + logger::ROUTING, + "Channel ID %u not found while adding FlexRay channel", + static_cast(channelId)); + continue; + } + + auto const index + = static_cast(::etl::distance(frChannelIds.begin(), it)); + + if ((flexrayReaders[index] == nullptr) + || (flexrayWriters[index] == nullptr)) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to add FlexRay channel (ID=%u,name=%s) due to invalid " + "reader or writer", + static_cast(channelId), + channelNameStr); + continue; + } + + ::routing::RxAdapterTable rxAdapterTable; + if (!::routing::load(config.data, rxAdapterTable)) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to load RX adapter table for channel %u", + static_cast(channelId)); + continue; + } + + logger::Logger::debug( + logger::ROUTING, + "Adding FlexRay channel (ID=%u,name=%s) to router", + static_cast(channelId), + channelNameStr); + + auto reader = &_flexrayRxAdapters[index].emplace( + *flexrayReaders[index], + rxAdapterTable, + ::routing::ErrorHandler(errorHandlingFunction, channelId)); + auto writer = &_flexrayTxAdapters[index].emplace( + *flexrayWriters[index], + txAdapterTable, + ::routing::ErrorHandler(errorHandlingFunction, channelId)); + + auto& flexrayChannelAdapter + = _flexrayChannelAdapters[_numberOfFlexrayChannels]; + flexrayChannelAdapter.channelId = channelId; + flexrayChannelAdapter.rxAdapter = reader; + flexrayChannelAdapter.txAdapter = writer; + + readers[channelId] = reader; + writers[channelId] = writer; + + ++_numberOfFlexrayChannels; + } + break; + default: + { + // unsupported channel type + } + break; + } + } + } + + _router.init(routingTable, readers, writers); + + _initialized = true; + } + + bool route() + { + if (!_initialized) + { + return false; + } + + return _router.run(); + } + + ::etl::span pduTransportChannelAdapters() const + { + if (!_initialized) + { + return {}; + } + + return ::etl::make_span(_pduTransportChannelAdapters).first(_numberOfPduTransportChannels); + } + + ::etl::span canChannelAdapters() const + { + if (!_initialized) + { + return {}; + } + + return ::etl::make_span(_canChannelAdapters).first(_numberOfCanChannels); + } + + ::etl::span flexrayChannelAdapters() const + { + if (!_initialized) + { + return {}; + } + + return ::etl::make_span(_flexrayChannelAdapters).first(_numberOfFlexrayChannels); + } + +private: + bool _initialized; + + uint8_t _numberOfPduTransportChannels; + uint8_t _numberOfCanChannels; + uint8_t _numberOfFlexrayChannels; + + ::etl::array<::etl::optional, MAX_NUM_PDU_TRANSPORT_CHANNELS> + _pduTransportRxAdapters; + ::etl::array<::etl::optional, MAX_NUM_PDU_TRANSPORT_CHANNELS> + _pduTransportTxAdapters; + ::etl::array + _pduTransportChannelAdapters; + + ::etl::array<::etl::optional, NUM_CAN_CHANNELS> _canRxAdapters; + ::etl::array<::etl::optional, NUM_CAN_CHANNELS> _canTxAdapters; + ::etl::array _canChannelAdapters; + + ::etl::array<::etl::optional, NUM_FLEXRAY_CHANNELS> _flexrayRxAdapters; + ::etl::array<::etl::optional, NUM_FLEXRAY_CHANNELS> _flexrayTxAdapters; + ::etl::array _flexrayChannelAdapters; + + ::etl::array<::io::IReader*, MAX_NUM_CHANNELS> _readers; + ::etl::array<::io::IWriter*, MAX_NUM_CHANNELS> _writers; + + Router _router; +}; + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/LegacyRxAdapter.h b/libs/bsw/routing/include/routing/LegacyRxAdapter.h new file mode 100644 index 00000000000..15b601f70a7 --- /dev/null +++ b/libs/bsw/routing/include/routing/LegacyRxAdapter.h @@ -0,0 +1,67 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/ErrorHandler.h" +#include "routing/RxAdapter.h" +#include "routing/RxAdapterTable.h" + +#include +#include +#include + +#include + +namespace routing +{ +/** + * Extracts the PDUs contained in the messages read from inputReader, adapting the IReader + * interface, according to the provided RxAdapterTable. + * + * [TPARAMS_BEGIN] + * \tparam MAX_ELEMENT_SIZE The maximum size of an extracted PDU. + * [TPARAMS_END] + */ +template +class LegacyRxAdapter : public RxAdapter +{ +public: + // [PUBLIC_API_BEGIN] + /** + * Construct an LegacyRxAdapter with a pre-filled RxAdapterTable. + */ + LegacyRxAdapter( + ::io::IReader& inputReader, RxAdapterTable const& table, ErrorHandler errorHandler); + + /** + * Return the MAX_ELEMENT_SIZE + */ + size_t maxSize() const override; + + // [PUBLIC_API_END] + +private: + ::etl::array _pduBuffer; +}; + +template +LegacyRxAdapter::LegacyRxAdapter( + ::io::IReader& inputReader, RxAdapterTable const& table, ErrorHandler const errorHandler) +: RxAdapter(inputReader, _pduBuffer, table, errorHandler) +{} + +template +size_t LegacyRxAdapter::maxSize() const +{ + return MAX_ELEMENT_SIZE; +} + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/LegacyTxAdapter.h b/libs/bsw/routing/include/routing/LegacyTxAdapter.h new file mode 100644 index 00000000000..01bd817272e --- /dev/null +++ b/libs/bsw/routing/include/routing/LegacyTxAdapter.h @@ -0,0 +1,108 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/ErrorHandler.h" +#include "routing/TxAdapterTable.h" +#include "routing/util.h" + +#include +#include + +#include + +namespace routing +{ +/** + * Adapt the IWriter to create PDUs of the correct size, collect statistics and report errors + * according to the behaviour expected for a legacy bus like CAN or FlexRay. + * + * The contents of an incoming PDU will be offset in a larger PDU according to the TxAdapterTable. + * The incoming PDUs contain their ID followed by the length, both as a big endian uint32_t, and + * then the payload. + * The outgoing PDUs contain the same four bytes of ID, four bytes of the new length, and the + * payload is offset inside a larger one where the extra bytes are set to 0xFF. + */ +class LegacyTxAdapter : public ::io::IWriter +{ +public: + // [PUBLIC_API_BEGIN] + /** + * Construct a LegacyTxAdapter from a pre-filled TxAdapterTable. + */ + LegacyTxAdapter( + ::io::IWriter& outputWriter, + ::routing::TxAdapterTable const& table, + ErrorHandler errorHandler); + + /** + * Return the maxSize() of the underlying writer. + */ + size_t maxSize() const override; + + /** + * Try to allocate max_size() of the underlying writer, and return a subspan of this memory. + * This will fail if there is not at least max_size() available in the underlying writer. + */ + ::etl::span allocate(size_t size) override; + + /** + * Find the correct message in the table, allocate, move the buffered PDU in the + * correct place, and fill the rest with 0xFF. + */ + void commit() override; + + /** + * Call flush on the underlying IWriter. + */ + void flush() override; + + // The PDU and byte counters keep track of the total amount of data written to the output queue. + + StatCounter::Type pduCounter() const { return _pduCounter; } + + StatCounter::Type byteCounter() const { return _byteCounter; } + + // The dropped PDU and byte counters store the amount of data that was not transmitted + // because allocation or commit failed. This occurs when the output queue is full, output + // message ID is unknown or PDU offset is invalid. + StatCounter::Type oversizedPdus() const { return _oversizedPdus; } + + StatCounter::Type failedMemAllocPdus() const { return _failedMemAllocPdus; } + + StatCounter::Type unknownMessagePdus() const { return _unknownMessagePdus; } + + StatCounter::Type failedMemMovePdus() const { return _failedMemMovePdus; } + + StatCounter::Type droppedPduCounter() const { return _droppedPduCounter; } + + StatCounter::Type droppedByteCounter() const { return _droppedByteCounter; } + + // [PUBLIC_API_END] +private: + ::io::IWriter& _outputWriter; + ::routing::TxAdapterTable _table; + ErrorHandler const _errorHandler; + ::etl::span _buffer; + StatCounter _pduCounter; + StatCounter _byteCounter; + + mutable StatCounter _oversizedPdus; + mutable StatCounter _failedMemAllocPdus; + mutable StatCounter _unknownMessagePdus; + mutable StatCounter _failedMemMovePdus; + + StatCounter _droppedPduCounter; + StatCounter _droppedByteCounter; + bool _lastAllocationFailed; +}; + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/Logger.h b/libs/bsw/routing/include/routing/Logger.h new file mode 100644 index 00000000000..9cc6af702b4 --- /dev/null +++ b/libs/bsw/routing/include/routing/Logger.h @@ -0,0 +1,13 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once +#include +DECLARE_LOGGER_COMPONENT(ROUTING) diff --git a/libs/bsw/routing/include/routing/PduRoutingTable.h b/libs/bsw/routing/include/routing/PduRoutingTable.h new file mode 100644 index 00000000000..5d093710309 --- /dev/null +++ b/libs/bsw/routing/include/routing/PduRoutingTable.h @@ -0,0 +1,59 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include +#include + +#include + +namespace routing +{ +// [PDU_ROUTING_TABLE_BEGIN] +/** + * The input of the routing is the output of the adapters, i.e. Internal PDU IDs. + * + * The routing table consists of two parts: + * + * 1) Routing to destinations. + * The internal PDU ID is used as index into destinationOffsets. The range + * destinations[destinationOffsets[pduId], destinationOffsets[pduId + 1][ are the + * destination channel indices to which the PDU is routed. + * + * 2) Translation from internal PDU ID to destination channel specific addressing. + * The index of a routing in the destinations table is also used in the outputMessageIds table. + * This contains the message ID to output on the channel for this message. + * + * The sizes of the spans are expressed according to: + * - N: the number of known PDUs + * - M: the number of channels + */ +struct PduRoutingTable +{ + // size: [N,(M*N)] + ::etl::span destinations; + + // size: N+1 + ::etl::span<::etl::be_uint32_t const> destinationOffsets; + + // size: [N, (M*N)] + ::etl::span<::etl::be_uint32_t const> outputMessageIds; +}; + +// [PDU_ROUTING_TABLE_END] + +/** + * Loads the PDU routing table from mem. + * Returns true on success and false on error. + */ +bool load(::etl::span mem, ::routing::PduRoutingTable& table); + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/PduTransportBufferedWriter.h b/libs/bsw/routing/include/routing/PduTransportBufferedWriter.h new file mode 100644 index 00000000000..664979bee0e --- /dev/null +++ b/libs/bsw/routing/include/routing/PduTransportBufferedWriter.h @@ -0,0 +1,124 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "io/IWriter.h" +#include "routing/util.h" + +#include +#include + +#include + +namespace routing +{ +/** + * Class for writing smaller chunks of bytes to a destination capable of transmitting + * larger pieces of data. The destination is meant to be the output queue of a PDU transport channel + * with a transmission timeout. Therefore, an instance of PduTransportBufferedWriter keeps track of + * the timestamp of the creation of the payload of a frame. + * + * A PduTransportBufferedWriter will always allocate destination.max_size() bytes as a buffer and + * then use this to give away chunks of data when allocating from it. Since the size of a PDU + * varies, it makes sense to pack multiple PDUs into one Ethernet frame to minimize the protocol + * overhead. + * + * When an allocation cannot guarantee destination.max_size() bytes, it flushes the current buffer + * and allocates a new full sized chunk from the destination. A flush writes the data to the + * destination. Hence, the current buffer is also flushed if the transmission timeout expires. + */ +class PduTransportBufferedWriter : public ::io::IWriter +{ +public: + // [PUBLIC_API_BEGIN] + /** + * Constructs a PduTransportBufferedWriter from a given IWriter destination. + */ + explicit PduTransportBufferedWriter(IWriter& destination, uint16_t transmissionTimeoutInMs); + + /** + * \see IWriter::maxSize() + */ + size_t maxSize() const override; + + /** + * Allocates a span of bytes of a given size. + * + * If not enough memory is available in the current buffer, it will be flushed and a + * new allocation of destination.max_size() bytes will be done. + * + * \param size Number of bytes to allocate from this BufferedWriter. + * \return - Empty span, if requested size was greater as MAX_ELEMENT_SIZE or no memory + * is available + * - Span of bytes otherwise. + */ + ::etl::span allocate(size_t size) override; + + /** + * Commits the previously allocated data and update the timestamp if this is first chunk + * of the current buffer. Unless the transmission timeout is equal to zero, it is not + * guaranteed that this data is immediately available to the reader as a call to flush() + * might be required for that. + */ + void commit() override; + + /** + * Commits the data currently written to a buffer allocated from the destination making it + * available to the reader. + */ + void flush() override; + + /** + * Flushes the current buffer if the transmission timeout has expired since its first chunk of + * data was committed or if it will expire in under 1ms. + */ + void checkTransmissionTimeout(); + + /** + * Resets the writer to its initial state. + */ + void reset(); + + // statistics + StatCounter::Type timeoutSends() const { return _timeoutSends; } + + StatCounter::Type flushes() const { return _flushes; } + + // [PUBLIC_API_END] + +private: + IWriter& _destination; + uint32_t const _transmissionTimeout; + + ::etl::span _bufferRemainder; + bool _isBufferEmpty; + size_t _currentPduSize; + uint32_t _timestamp; + + StatCounter _timeoutSends; + StatCounter _flushes; +}; + +inline PduTransportBufferedWriter::PduTransportBufferedWriter( + IWriter& destination, uint16_t const transmissionTimeoutInMs) +: _destination(destination) +, _transmissionTimeout(transmissionTimeoutInMs * 1000U) +, _bufferRemainder() +, _isBufferEmpty(true) +, _currentPduSize(0) +, _timestamp(0) +, _timeoutSends() +, _flushes() +{} + +inline size_t PduTransportBufferedWriter::maxSize() const { return _destination.maxSize(); } + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/PduTransportConfig.h b/libs/bsw/routing/include/routing/PduTransportConfig.h new file mode 100644 index 00000000000..f250b9f29a9 --- /dev/null +++ b/libs/bsw/routing/include/routing/PduTransportConfig.h @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/Header.h" + +#include +#include +#include + +#include + +namespace routing +{ +struct PduTransportConfig +{ + enum class Mode : uint8_t + { + RX = 0x01, + TX = 0x10, + RX_TX = 0x11 + }; + + uint8_t channelId = INVALID_CHANNEL_ID; + ::ip::IPAddress ipAddress = {}; + ::etl::be_uint16_t vlanId = {}; + ::etl::be_uint16_t localPort = {}; + ::etl::be_uint16_t remotePort = {}; + ::etl::be_uint16_t transmissionTimeout = {}; + Mode mode = Mode::RX_TX; + uint8_t type = 0; + uint8_t pcp = 0; + ::etl::span remoteIpAddresses = {}; +}; + +/** + * Loads the PDU transport channel config from mem. + * Returns true on success and false on error. + * On failure, the config may be partially modified and must not be used. + */ +bool load(::etl::span mem, PduTransportConfig& config); +} // namespace routing diff --git a/libs/bsw/routing/include/routing/PduTransportIntegration.h b/libs/bsw/routing/include/routing/PduTransportIntegration.h new file mode 100644 index 00000000000..d616c12d78a --- /dev/null +++ b/libs/bsw/routing/include/routing/PduTransportIntegration.h @@ -0,0 +1,392 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace routing +{ +namespace logger = ::util::logger; + +template +class PduTransportIntegration +{ +public: + using SIZE_TYPE = uint16_t; + // Queue should always be able to store at least 2 elements. Because of the worst case scenario + // of the memory queue, this requires 2 extra elements of MAX_ELEMENT_SIZE worth of space. + static constexpr size_t CAPACITY = 4 * (MAX_ELEMENT_SIZE + sizeof(SIZE_TYPE)); + using Queue = ::io::MemoryQueue; + using Reader = ::io::MemoryQueueReader; + using Writer = ::io::MemoryQueueWriter; + + PduTransportIntegration() + : _initialized(false) + , _inputSockets() + , _outputSockets() + , _pduTransportChannelConfigs() + , _udpReceivers() + , _udpSenders() + , _udpListeners() + , _rxQueues() + , _txQueues() + , _rxReaders() + , _txReaders() + , _rxWriters() + , _txWriters() + , _pduTransportBufferedTxWriters() + { + for (size_t i = 0; i < MAX_NUM_CHANNELS; ++i) + { + auto& rxQueue = _rxQueues.emplace_back(); + (void)_rxReaders.emplace_back(rxQueue); + (void)_rxWriters.emplace_back(rxQueue); + + auto& txQueue = _txQueues.emplace_back(); + (void)_txReaders.emplace_back(txQueue); + (void)_txWriters.emplace_back(txQueue); + } + } + + void init( + ::etl::span const blob, + ::etl::span channelIds, + ::etl::span<::udp::AbstractDatagramSocket* const> inputSockets, + ::etl::span<::udp::AbstractDatagramSocket* const> outputSockets) + { + if (_initialized) + { + logger::Logger::warn(logger::ROUTING, "PDU TP integration already initialized"); + return; + } + + size_t const numberOfChannels = ::etl::min( + static_cast(MAX_NUM_CHANNELS), + ::etl::min(channelIds.size(), ::etl::min(inputSockets.size(), outputSockets.size()))); + channelIds = channelIds.first(numberOfChannels); + inputSockets = inputSockets.first(numberOfChannels); + outputSockets = outputSockets.first(numberOfChannels); + + if ((::etl::find(inputSockets.begin(), inputSockets.end(), nullptr) != inputSockets.end()) + || (::etl::find(outputSockets.begin(), outputSockets.end(), nullptr) + != outputSockets.end())) + { + logger::Logger::warn(logger::ROUTING, "Invalid socket(s) at initialization"); + } + + _inputSockets = inputSockets; + _outputSockets = outputSockets; + + for (size_t i = 0; i < numberOfChannels; ++i) + { + auto const channelConfig + = ::routing::config(blob, ::blob::Config::Type::CHANNEL, channelIds[i]); + auto const channelConfigData + = _pduTransportChannelConfigs.emplace_back(channelConfig.data); + + ::routing::PduTransportConfig pduTransportConfig; + if (!::routing::load(channelConfigData, pduTransportConfig)) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to load PDU TP channel %u config", + static_cast(i)); + } + + (void)_pduTransportBufferedTxWriters.emplace_back( + _txWriters[i], pduTransportConfig.transmissionTimeout); + } + + _initialized = true; + } + + ::etl::span inputQueues() const { return _rxQueues; } + + ::etl::span outputQueues() const { return _txQueues; } + + ::etl::span inputReaders() { return _rxReaders; } + + ::etl::span inputWriters() { return _rxWriters; } + + ::etl::span outputWriters() { return _txWriters; } + + ::etl::span<::routing::PduTransportBufferedWriter> bufferedOutputWriters() + { + return _pduTransportBufferedTxWriters; + } + + ::etl::span<::io::udp::Receiver const> udpReceivers() const { return _udpReceivers; } + + ::etl::span<::io::udp::Sender const> udpSenders() const { return _udpSenders; } + + void checkTransmissionTimeouts() + { + if (!_initialized) + { + logger::Logger::error(logger::ROUTING, "PT integration uninitialized"); + return; + } + + for (auto& writer : _pduTransportBufferedTxWriters) + { + writer.checkTransmissionTimeout(); + } + } + + void sendUdpFrames() + { + if (!_initialized) + { + logger::Logger::error(logger::ROUTING, "PduTp integration uninitialized"); + return; + } + + constexpr size_t MAX_NUM_FRAMES = 10; + for (auto& udpSender : _udpSenders) + { + udpSender.run(MAX_NUM_FRAMES); + } + } + + void activate( + ::ip::IPAddress const& localIpAddress, + uint16_t const vlanId, + ::routing::ErrorHandler::Function const errorHandlingFunction) + { + if (!_initialized) + { + logger::Logger::error(logger::ROUTING, "PT integration uninitialized"); + return; + } + + for (size_t i = 0; i < _pduTransportChannelConfigs.size(); ++i) + { + ::routing::PduTransportConfig pduTransportConfig; + if (!::routing::load(_pduTransportChannelConfigs[i], pduTransportConfig)) + { + logger::Logger::warn( + logger::ROUTING, + "Failed to activate PDU TP channel %u due to invalid config", + static_cast(i)); + continue; + } + + if (pduTransportConfig.vlanId != vlanId) + { + continue; + } + + clear(_rxReaders[i]); + clear(_txReaders[i]); + _pduTransportBufferedTxWriters[i].reset(); + + bool const isRxTx + = pduTransportConfig.mode == ::routing::PduTransportConfig::Mode::RX_TX; + bool const isRx + = isRxTx || (pduTransportConfig.mode == ::routing::PduTransportConfig::Mode::RX); + bool const isTx + = isRxTx || (pduTransportConfig.mode == ::routing::PduTransportConfig::Mode::TX); + + if (isRx) + { + enableRx(i, pduTransportConfig, localIpAddress, errorHandlingFunction); + } + + if (isTx) + { + enableTx(i, pduTransportConfig, localIpAddress); + } + } + } + + void disable() + { + if (!_initialized) + { + logger::Logger::error(logger::ROUTING, "PDU TP integration uninitialized"); + return; + } + + for (size_t i = 0; i < _pduTransportChannelConfigs.size(); ++i) + { + if (_outputSockets[i] != nullptr) + { + _outputSockets[i]->disconnect(); + _outputSockets[i]->close(); + } + + if (_inputSockets[i] != nullptr) + { + _inputSockets[i]->close(); + } + } + + _udpListeners.clear(); + _udpSenders.clear(); + _udpReceivers.clear(); + } + +private: + class DiscardingListener : private ::udp::IDataListener + { + public: + explicit DiscardingListener(::udp::AbstractDatagramSocket& socket) : _socket(socket) + { + _socket.setDataListener(this); + } + + virtual ~DiscardingListener() {} + + private: + void dataReceived( + ::udp::AbstractDatagramSocket& /* socket */, + ::ip::IPAddress /*sourceAddress*/, + uint16_t const /*sourcePort*/, + ::ip::IPAddress /*destinationAddress*/, + uint16_t const length) override + { + (void)_socket.read(nullptr, length); + } + + ::udp::AbstractDatagramSocket& _socket; + }; + + static void clear(::io::IReader& reader) + { + while (!reader.peek().empty()) + { + reader.release(); + } + } + + void enableRx( + size_t const index, + ::routing::PduTransportConfig const& pduTransportConfig, + ::ip::IPAddress const& localIpAddress, + ::routing::ErrorHandler::Function const errorHandlingFunction) + { + auto const inputSocket = _inputSockets[index]; + + if (inputSocket == nullptr) + { + logger::Logger::warn( + logger::ROUTING, "Invalid input socket %u", static_cast(index)); + return; + } + + (void)_udpReceivers.emplace_back( + *inputSocket, + _rxWriters[index], + ::routing::ErrorHandler(errorHandlingFunction, pduTransportConfig.channelId), + pduTransportConfig.remoteIpAddresses); + + bool const isBound = inputSocket->bind(&localIpAddress, pduTransportConfig.remotePort) + == udp::AbstractDatagramSocket::ErrorCode::UDP_SOCKET_OK; + if (!isBound) + { + logger::Logger::error( + logger::ROUTING, "Failed to bind input socket %u", static_cast(index)); + return; + } + + bool const isJoined = inputSocket->join(pduTransportConfig.ipAddress) + == udp::AbstractDatagramSocket::ErrorCode::UDP_SOCKET_OK; + if (!isJoined) + { + logger::Logger::error( + logger::ROUTING, + "Failed to join socket %u to multicast address", + static_cast(index)); + return; + } + } + + void enableTx( + size_t const index, + ::routing::PduTransportConfig const& pduTransportConfig, + ::ip::IPAddress const& localIpAddress) + { + auto const outputSocket = _outputSockets[index]; + + if (outputSocket == nullptr) + { + logger::Logger::warn( + logger::ROUTING, "Invalid output socket %u", static_cast(index)); + return; + } + + (void)_udpListeners.emplace_back(*outputSocket); + + bool const isBound = outputSocket->bind(&localIpAddress, pduTransportConfig.localPort) + == udp::AbstractDatagramSocket::ErrorCode::UDP_SOCKET_OK; + if (!isBound) + { + logger::Logger::error( + logger::ROUTING, "Failed to bind output socket %u", static_cast(index)); + return; + } + + bool const isConnected = outputSocket->connect( + pduTransportConfig.ipAddress, + pduTransportConfig.remotePort, + const_cast<::ip::IPAddress*>(&localIpAddress)) + == udp::AbstractDatagramSocket::ErrorCode::UDP_SOCKET_OK; + + if (!isConnected) + { + logger::Logger::error( + logger::ROUTING, + "Failed to connect output socket %u to multicast address", + static_cast(index)); + return; + } + + (void)_udpSenders.emplace_back(_txReaders[index], *outputSocket); + } + + bool _initialized; + + ::etl::span<::udp::AbstractDatagramSocket* const> _inputSockets; + ::etl::span<::udp::AbstractDatagramSocket* const> _outputSockets; + + ::etl::vector<::etl::span, MAX_NUM_CHANNELS> _pduTransportChannelConfigs; + + ::etl::vector<::io::udp::Receiver, MAX_NUM_CHANNELS> _udpReceivers; + ::etl::vector<::io::udp::Sender, MAX_NUM_CHANNELS> _udpSenders; + ::etl::vector _udpListeners; + + ::etl::vector _rxQueues; + ::etl::vector _txQueues; + ::etl::vector _rxReaders; + ::etl::vector _txReaders; + ::etl::vector _rxWriters; + ::etl::vector _txWriters; + ::etl::vector<::routing::PduTransportBufferedWriter, MAX_NUM_CHANNELS> + _pduTransportBufferedTxWriters; +}; + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/PduTransportRxAdapter.h b/libs/bsw/routing/include/routing/PduTransportRxAdapter.h new file mode 100644 index 00000000000..0835612f523 --- /dev/null +++ b/libs/bsw/routing/include/routing/PduTransportRxAdapter.h @@ -0,0 +1,247 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/ErrorHandler.h" +#include "routing/RxAdapter.h" +#include "routing/RxAdapterTable.h" +#include "routing/util.h" + +#include +#include +#include +#include +#include + +#include + +namespace routing +{ +/** + * Extracts the PDUs contained in the messages read from inputReader, adapting the IReader + * interface, according to the provided RxAdapterTable. + * + * [TPARAMS_BEGIN] + * \tparam MAX_ELEMENT_SIZE The maximum size of an extracted PDU. + * [TPARAMS_END] + */ +template +class PduTransportRxAdapter : public RxAdapter +{ +public: + // [PUBLIC_API_BEGIN] + /** + * Construct a PduTransportRxAdapter with a pre-filled RxAdapterTable. + */ + PduTransportRxAdapter( + ::io::IReader& inputReader, RxAdapterTable const& table, ErrorHandler errorHandler); + + /** + * Return the MAX_ELEMENT_SIZE + */ + size_t maxSize() const override; + + ::etl::span peek() const override; + + // The discarded PDU and byte counters store the amount of data in incoming messages out of + // which not a single PDU was extracted. + StatCounter::Type invalidMessageLengthPdus() const { return _invalidMessageLengthPdus; } + + // [PUBLIC_API_END] + +protected: + ErrorHandler::StatusCode lookup(::etl::span message) const override; + +private: + ErrorHandler::StatusCode processMessage(::etl::span frame) const; + + ::etl::array _pduBuffer; + + mutable ::etl::span _currentFrame; + mutable size_t _currentMessageSize; + mutable uint32_t _invalidMessageLengthPdus; +}; + +template +PduTransportRxAdapter::PduTransportRxAdapter( + ::io::IReader& inputReader, RxAdapterTable const& table, ErrorHandler const errorHandler) +: RxAdapter(inputReader, _pduBuffer, table, errorHandler) +, _currentFrame() +, _currentMessageSize() +, _invalidMessageLengthPdus() +{} + +template +size_t PduTransportRxAdapter::maxSize() const +{ + return MAX_ELEMENT_SIZE; +} + +template +ErrorHandler::StatusCode +PduTransportRxAdapter::processMessage(::etl::span frame) const +{ + if (frame.size() < MESSAGE_HEADER_SIZE) + { + _currentMessageSize = frame.size(); + + ++_invalidHeaderSizePdus; + + return ErrorHandler::StatusCode::INVALID_MESSAGE_HEADER_SIZE; + } + + auto header = frame.first(MESSAGE_HEADER_SIZE); + + (void)header.take<::etl::be_uint32_t const>(); // message ID + auto const parsedPayloadLength = header.take<::etl::be_uint32_t const>(); + auto const messageSize = MESSAGE_HEADER_SIZE + parsedPayloadLength; + if (messageSize > frame.size()) + { + _currentMessageSize = frame.size(); + + ++_invalidParsedPayloadLengthPdus; + + return ErrorHandler::StatusCode::INVALID_PARSED_LENGTH; + } + _currentMessageSize = messageSize; + + auto const message = frame.take(_currentMessageSize); + return lookup(message); +} + +template +::etl::span PduTransportRxAdapter::peek() const +{ + // Return the last PDU extracted. + if (!_currentPdu.empty()) + { + return _currentPdu; + } + + if (_currentFrame.empty()) + { + _currentFrame = _inputReader.peek(); + } + + while (!_currentFrame.empty()) + { + bool const isNewMessage = _currentPduIndex == 0; + if (isNewMessage) + { + auto const statusCode = processMessage(_currentFrame); + + ++_pduCounter; + _byteCounter += static_cast(_currentMessageSize); + + if (statusCode != ErrorHandler::StatusCode::OK) + { + _errorHandler(statusCode, _currentFrame.first(_currentMessageSize)); + + ++_discardedPduCounter; + _discardedByteCounter += static_cast(_currentMessageSize); + + _currentFrame.advance(_currentMessageSize); + + if (_currentFrame.empty()) + { + _inputReader.release(); + _currentFrame = _inputReader.peek(); + } + + continue; + } + } + + bool noPduExtracted = true; + auto const numberOfPdus = _table.pduLengthsOffsets[_currentMessageIndex + 1] + - _table.pduLengthsOffsets[_currentMessageIndex]; + auto const message = _currentFrame.first(_currentMessageSize); + while (noPduExtracted && (_currentPduIndex < numberOfPdus)) + { + auto const channelSpecificPduId + = _table.pduLengthsOffsets[_currentMessageIndex] + _currentPduIndex; + auto pdu = _buffer; + if (extract(message, channelSpecificPduId, pdu)) + { + _currentPdu = pdu; + noPduExtracted = false; + } + + ++_currentPduIndex; + } + + if (_currentPduIndex == numberOfPdus) + { + _currentPduIndex = 0; + _currentFrame.advance(_currentMessageSize); + + if (_currentFrame.empty()) + { + _inputReader.release(); + _currentFrame = _inputReader.peek(); + } + } + + if (noPduExtracted) + { + if (isNewMessage) + { + // Although this message was expected, no PDU was extracted from it because all the + // expected PDUs were too big + _errorHandler(ErrorHandler::StatusCode::INVALID_PAYLOAD_LENGTH, message); + + ++_invalidPayloadLengthPdus; + + ++_discardedPduCounter; + _discardedByteCounter += static_cast(_currentMessageSize); + } + + continue; + } + + return _currentPdu; + } + return {}; +} + +template +ErrorHandler::StatusCode +PduTransportRxAdapter::lookup(::etl::span message) const +{ + auto const messageId = message.take<::etl::be_uint32_t const>(); + auto const pos = ::etl::find(_table.messageIds.begin(), _table.messageIds.end(), messageId); + if (pos == _table.messageIds.end()) + { + ++_unknownMessagePdus; + + return ErrorHandler::StatusCode::UNKNOWN_MESSAGE_ID; + } + + uint32_t const parsedPayloadLength = message.take<::etl::be_uint32_t const>(); + + auto const index = static_cast(::etl::distance(_table.messageIds.begin(), pos)); + uint32_t const messageLength = (!_table.messageLengths.empty()) + ? static_cast(_table.messageLengths[index]) + : 0U; + + if ((messageLength != 0) && (parsedPayloadLength != messageLength)) + { + ++_invalidMessageLengthPdus; + + return ErrorHandler::StatusCode::INVALID_MESSAGE_LENGTH; + } + + _currentMessageIndex = index; + + return ErrorHandler::StatusCode::OK; +} + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/PduTransportTxAdapter.h b/libs/bsw/routing/include/routing/PduTransportTxAdapter.h new file mode 100644 index 00000000000..e0e4869a507 --- /dev/null +++ b/libs/bsw/routing/include/routing/PduTransportTxAdapter.h @@ -0,0 +1,71 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/ErrorHandler.h" +#include "routing/util.h" + +#include +#include + +#include + +namespace routing +{ +/** + * Adapts an IWriter to count sent pdus and bytes. + */ +class PduTransportTxAdapter : public ::io::IWriter +{ +public: + // [PUBLIC_API_BEGIN] + /** + * All the methds delegate to the underlying IWriter, while counting the bytes. + */ + PduTransportTxAdapter(::io::IWriter& outputWriter, ErrorHandler errorHandler); + + size_t maxSize() const override; + + ::etl::span allocate(size_t size) override; + + void commit() override; + + void flush() override; + + // The PDU and byte counters keep track of the total amount of data written to the output queue. + StatCounter::Type pduCounter() const { return _pduCounter; } + + StatCounter::Type byteCounter() const { return _byteCounter; } + + // The dropped PDU and byte counters store the amount of data that was not transmitted + // because the output queue was full. + StatCounter::Type failedMemAllocPdus() const { return _failedMemAllocPdus; } + + StatCounter::Type droppedPduCounter() const { return _droppedPduCounter; } + + StatCounter::Type droppedByteCounter() const { return _droppedByteCounter; } + + // [PUBLIC_API_END] +private: + ::io::IWriter& _outputWriter; + ErrorHandler const _errorHandler; + size_t _currentPduSize; + StatCounter _pduCounter; + StatCounter _byteCounter; + + mutable StatCounter _failedMemAllocPdus; + + StatCounter _droppedPduCounter; + StatCounter _droppedByteCounter; + bool _lastAllocationFailed; +}; + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/Router.h b/libs/bsw/routing/include/routing/Router.h new file mode 100644 index 00000000000..2cd18cd19b0 --- /dev/null +++ b/libs/bsw/routing/include/routing/Router.h @@ -0,0 +1,177 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/Logger.h" +#include "routing/PduRoutingTable.h" +#include "routing/pduRouting.h" + +#include +#include +#include +#include + +#include + +namespace routing +{ +/** + * A PDU Router that forwards PDUs from readers to writers according to its routing table. + * + * [TPARAMS_BEGIN] + * \tparam MAX_NUM_CHANNELS The maximum number of channels to write to and from. Needs to fit the + * routing table data. [TPARAMS_END] + */ +template +class Router +{ +public: + // [PUBLIC_API_BEGIN] + /** + * Construct an uninitialized Router. + */ + Router() = default; + + /** + * Initialize a Router using a pre-filled PduRoutingTable. + * + * It starts with transmission and reception enabled for all channels. + */ + void init( + ::routing::PduRoutingTable const& table, + ::etl::span<::io::IReader*> readers, + ::etl::span<::io::IWriter*> writers); + + /** + * Initialize a Router by loading the PduRoutingTable from the provided config byte array. + * + * If the config data is invalid, the Router will be uninitialized and the other functions will + * not do anything apart from logging an error. + * + * Otherwise, it starts with transmission and reception enabled for all channels. + */ + void init( + ::etl::span config, + ::etl::span<::io::IReader*> readers, + ::etl::span<::io::IWriter*> writers); + + /** + * Try routing a PDU from a channel. + * + * It returns true once a PDU is routed. Otherwise, it returns false after attempting once from + * each channel. + */ + bool run(); + + // [PUBLIC_API_END] +private: + ::routing::PduRoutingTable _table; + ::etl::span<::io::IReader*> _readers; + ::etl::span<::io::IWriter*> _writers; + bool _initialized = false; + uint8_t _currentChannelId = 0; +}; + +template +void Router::init( + ::routing::PduRoutingTable const& table, + ::etl::span<::io::IReader*> readers, + ::etl::span<::io::IWriter*> writers) +{ + if (_initialized) + { + ::util::logger::Logger::warn(::util::logger::ROUTING, "Router already initialized"); + return; + } + + size_t const numberOfChannels = ::etl::min( + static_cast(MAX_NUM_CHANNELS), ::etl::min(readers.size(), writers.size())); + readers = readers.first(numberOfChannels); + writers = writers.first(numberOfChannels); + + if ((::etl::find(readers.begin(), readers.end(), nullptr) != readers.end()) + || (::etl::find(writers.begin(), writers.end(), nullptr) != writers.end())) + { + ::util::logger::Logger::warn( + ::util::logger::ROUTING, "Invalid reader(s) or writer(s) at initialization"); + } + + if ((table.destinations.empty()) || (table.destinationOffsets.empty()) + || (table.outputMessageIds.empty())) + { + ::util::logger::Logger::error( + ::util::logger::ROUTING, "Failed to initialize router: empty routing configuration"); + return; + } + + if (table.destinations.size() != table.outputMessageIds.size()) + { + ::util::logger::Logger::error( + ::util::logger::ROUTING, + "Failed to initialize router: destinations and output message IDs have different " + "sizes"); + return; + } + + _table = table; + _readers = readers; + _writers = writers; + + _initialized = true; +} + +template +void Router::init( + ::etl::span const config, + ::etl::span<::io::IReader*> const readers, + ::etl::span<::io::IWriter*> const writers) +{ + if (_initialized) + { + ::util::logger::Logger::warn(::util::logger::ROUTING, "Router already initialized"); + return; + } + + if (!load(config, _table)) + { + ::util::logger::Logger::error( + ::util::logger::ROUTING, "Failed to load routing table from config"); + } + init(_table, readers, writers); +} + +template +bool Router::run() +{ + if (!_initialized) + { + ::util::logger::Logger::error(::util::logger::ROUTING, "Router uninitialized"); + return false; + } + + for (size_t i = 0; i < _readers.size(); ++i) + { + auto const reader = _readers[_currentChannelId]; + bool const pduRouted + = reader != nullptr ? route(_currentChannelId, _table, *reader, _writers) : false; + _currentChannelId = static_cast(_currentChannelId) + 1U < _readers.size() + ? _currentChannelId + 1U + : 0U; + if (pduRouted) + { + return true; + } + } + + return false; +} + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/RxAdapter.h b/libs/bsw/routing/include/routing/RxAdapter.h new file mode 100644 index 00000000000..df378ae4a98 --- /dev/null +++ b/libs/bsw/routing/include/routing/RxAdapter.h @@ -0,0 +1,119 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/ErrorHandler.h" +#include "routing/RxAdapterTable.h" +#include "routing/util.h" + +#include +#include + +#include + +namespace routing +{ +/** + * Extracts the PDUs contained in the messages read from inputReader, adapting the IReader + * interface, according to the provided RxAdapterTable. + */ +class RxAdapter : public ::io::IReader +{ +public: + static constexpr size_t MESSAGE_HEADER_SIZE = 8U; + + // [PUBLIC_API_BEGIN] + /** + * Construct an RxAdapter with a pre-filled RxAdapterTable. + */ + RxAdapter( + ::io::IReader& inputReader, + ::etl::span buffer, + RxAdapterTable const& table, + ErrorHandler errorHandler); + + /** + * Return the size of the buffer. + */ + size_t maxSize() const override; + + /** + * Each peek() call following a release() call will return the next PDU extracted from the + * current message of the IReader, according to the list defined in the RxAdapterTable. + * Once all the PDUs have been read, or if the current message is not known in the table, a new + * peek() call will be performed on the IReader, releasing the previous one. + * + * An empty span will be returned if nothing is available. + */ + ::etl::span peek() const override; + + /** + * Reset the internal PDU buffer, allowing the next peek() call to extract a new PDU from a + * message. + */ + void release() override; + + // The PDU and byte counters keep track of the total amount of data read from the input queue, + // regardless of the number of PDUs extracted. + StatCounter::Type pduCounter() const { return _pduCounter; } + + StatCounter::Type byteCounter() const { return _byteCounter; } + + // The discarded PDU and byte counters store the amount of data in incoming messages out of + // which not a single PDU was extracted. + StatCounter::Type invalidHeaderSizePdus() const { return _invalidHeaderSizePdus; } + + StatCounter::Type unknownMessagePdus() const { return _unknownMessagePdus; } + + StatCounter::Type invalidParsedPayloadLengthPdus() const + { + return _invalidParsedPayloadLengthPdus; + } + + StatCounter::Type invalidPayloadLengthPdus() const { return _invalidPayloadLengthPdus; } + + StatCounter::Type discardedPduCounter() const { return _discardedPduCounter; } + + StatCounter::Type discardedByteCounter() const { return _discardedByteCounter; } + + // [PUBLIC_API_END] + +protected: + virtual ErrorHandler::StatusCode lookup(::etl::span message) const; + + bool extract( + ::etl::span message, + size_t channelSpecificPduId, + ::etl::span& pdu) const; + + ::io::IReader& _inputReader; + ::etl::span const _buffer; + RxAdapterTable _table; + + ErrorHandler const _errorHandler; + + mutable size_t _currentMessageIndex; + mutable size_t _currentPduIndex; + mutable ::etl::span _currentPdu; + + mutable StatCounter _pduCounter; + mutable StatCounter _byteCounter; + + mutable StatCounter _invalidHeaderSizePdus; + mutable StatCounter _unknownMessagePdus; + mutable StatCounter _invalidParsedPayloadLengthPdus; + mutable StatCounter _invalidPayloadLengthPdus; + + mutable StatCounter _discardedPduCounter; + mutable StatCounter _discardedByteCounter; +}; + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/RxAdapterTable.h b/libs/bsw/routing/include/routing/RxAdapterTable.h new file mode 100644 index 00000000000..1874c531710 --- /dev/null +++ b/libs/bsw/routing/include/routing/RxAdapterTable.h @@ -0,0 +1,57 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include +#include + +#include + +namespace routing +{ +// [RX_ADAPTER_TABLE_BEGIN] +// The sizes below are expressed as a function of: +// - N: the number of known PDUs (>= P) +// - P: the number of known messages +struct RxAdapterTable +{ + // the first internal PDU ID of this channel. All other PDU IDs are derived from it. + ::etl::be_uint32_t firstId = {}; + // The message IDs that can be received. + // The index is called messageIndex. It is shared with messageLengths and pduLengthsOffsets. + // size: P + ::etl::span<::etl::be_uint32_t const> messageIds; + // The lengths of the messages that can be received. If the length of a message + // does not match the expected one, no PDU is extracted. If empty, it is ignored. + // size: P + ::etl::span<::etl::be_uint32_t const> messageLengths; + // A pointer into the PDULengths and PDUOffsets tables. + // size: P + 1 + ::etl::span<::etl::be_uint32_t const> pduLengthsOffsets; + // The length of each PDU in each frame. + // The index is called channelSpecificPduId. It is shared with pduOffsets and + // used to derive the internal PDU ID by adding it to firstId. + // size: N + ::etl::span<::etl::be_uint32_t const> pduLengths; + // The offset in each frame of each PDU. + // size: N + ::etl::span<::etl::be_uint32_t const> pduOffsets; +}; + +// [RX_ADAPTER_TABLE_END] + +/** + * Loads the RX adapter table from mem. + * Returns true on success and false on error. + */ +bool load(::etl::span mem, RxAdapterTable& table); + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/TxAdapterTable.h b/libs/bsw/routing/include/routing/TxAdapterTable.h new file mode 100644 index 00000000000..800a232964f --- /dev/null +++ b/libs/bsw/routing/include/routing/TxAdapterTable.h @@ -0,0 +1,44 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include +#include + +#include + +namespace routing +{ +// [TX_ADAPTER_TABLE_BEGIN] +// The sizes below are expresed as a function of: +// - N: the number of known PDUs +struct TxAdapterTable +{ + // The message IDs which can be sent by this adapter + // size: N + ::etl::span<::etl::be_uint32_t const> messageIds; + // The length of each message to be sent, achieved using padding with 0xFF. + // size: N + ::etl::span<::etl::be_uint32_t const> messageLengths; + // The offset of the PDU to be sent in the message. + // size: N + ::etl::span<::etl::be_uint32_t const> pduOffsets; +}; + +// [TX_ADAPTER_TABLE_END] + +/** + * Loads the TX adapter table from mem. + * Returns true on success and false on error. + */ +bool load(::etl::span mem, TxAdapterTable& table); + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/pduRouting.h b/libs/bsw/routing/include/routing/pduRouting.h new file mode 100644 index 00000000000..0ed686a8d8d --- /dev/null +++ b/libs/bsw/routing/include/routing/pduRouting.h @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/PduRoutingTable.h" + +#include +#include +#include +#include + +#include + +namespace routing +{ +bool route( + uint8_t srcChannelId, + PduRoutingTable const& table, + ::io::IReader& reader, + ::etl::span<::io::IWriter*> writers); + +void outputPdu( + ::etl::be_uint32_t outputMessageId, + uint8_t dstChannelId, + ::etl::span const payload, + ::io::IWriter& writer); + +} // namespace routing diff --git a/libs/bsw/routing/include/routing/util.h b/libs/bsw/routing/include/routing/util.h new file mode 100644 index 00000000000..1241201ae5b --- /dev/null +++ b/libs/bsw/routing/include/routing/util.h @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/ClampedCounter.h" +#include "routing/Header.h" + +#include +#include + +#include + +namespace routing +{ +using StatCounter = ClampedCounter; + +::blob::Config config( + ::etl::span blob, + ::blob::Config::Type type, + uint8_t channelId = ::routing::INVALID_CHANNEL_ID); + +} // namespace routing diff --git a/libs/bsw/routing/module.spec b/libs/bsw/routing/module.spec new file mode 100644 index 00000000000..87faef85b71 --- /dev/null +++ b/libs/bsw/routing/module.spec @@ -0,0 +1 @@ +oss: true diff --git a/libs/bsw/routing/src/routing/ChannelNames.cpp b/libs/bsw/routing/src/routing/ChannelNames.cpp new file mode 100644 index 00000000000..892f9b51b26 --- /dev/null +++ b/libs/bsw/routing/src/routing/ChannelNames.cpp @@ -0,0 +1,80 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/ChannelNames.h" + +#include "routing/Header.h" + +#include + +namespace routing +{ +::etl::span name(ChannelNames const& config, uint8_t const channelId) +{ + if (config.offsets.empty()) + { + return {}; + } + + auto const namesCount = config.offsets.size() - 1U; + if (channelId >= namesCount) + { + return {}; + } + + auto const currentOffset = static_cast(config.offsets[channelId]); + auto const nextOffset + = static_cast(config.offsets[static_cast(channelId) + 1U]); + if ((nextOffset <= currentOffset) || (config.names.size() < nextOffset)) + { + return {}; + } + + return config.names.subspan(currentOffset, nextOffset - currentOffset); +} + +bool load(::etl::span mem, ChannelNames& channelNames) +{ + if (mem.empty()) + { + return false; + } + + ::routing::Header header; + (void)::routing::load(mem, header); + + if (header.configType != ::blob::ConfigType::CHANNEL_NAMES) + { + return false; + } + + if (mem.size() < sizeof(::etl::be_uint16_t)) + { + return false; + } + + auto const namesCount = static_cast(mem.take<::etl::be_uint16_t const>()); + if (mem.size() < (namesCount + 1U) * sizeof(::etl::be_uint16_t)) + { + return false; + } + + channelNames.offsets = mem.take<::etl::be_uint16_t const>(namesCount + 1U); + + if (mem.size() < channelNames.offsets[namesCount]) + { + return false; + } + + channelNames.names = mem.take(channelNames.offsets[namesCount]); + + return true; +} +} // namespace routing diff --git a/libs/bsw/routing/src/routing/Header.cpp b/libs/bsw/routing/src/routing/Header.cpp new file mode 100644 index 00000000000..95a05674235 --- /dev/null +++ b/libs/bsw/routing/src/routing/Header.cpp @@ -0,0 +1,65 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/Header.h" + +#include +#include + +namespace routing +{ +bool load(::etl::span& mem, Header& header) +{ + if (mem.size() < sizeof(::blob::Config::Header)) + { + return false; + } + + auto configHeaderMem = mem.take(sizeof(::blob::Config::Header)); + + uint32_t const configTypeValue = configHeaderMem.take<::etl::be_uint32_t const>(); + + if ((configTypeValue != static_cast(::blob::ConfigType::ROUTING)) + && (configTypeValue != static_cast(::blob::ConfigType::RX_ADAPTER)) + && (configTypeValue != static_cast(::blob::ConfigType::TX_ADAPTER)) + && (configTypeValue != static_cast(::blob::ConfigType::CHANNEL)) + && (configTypeValue != static_cast(::blob::ConfigType::CHANNEL_NAMES)) + && (configTypeValue != static_cast(::blob::ConfigType::META))) + { + return false; + } + + header.configType = static_cast<::blob::ConfigType>(configTypeValue); + + uint16_t const channelTypeValue = configHeaderMem.take<::etl::be_uint16_t const>(); + + // unused + (void)configHeaderMem.advance(sizeof(uint8_t)); + + auto const channelId = configHeaderMem.take(); + + if ((header.configType == ::blob::ConfigType::CHANNEL) + || (header.configType == ::blob::ConfigType::RX_ADAPTER) + || (header.configType == ::blob::ConfigType::TX_ADAPTER)) + { + if ((channelTypeValue != static_cast(::routing::ChannelType::PDU_TRANSPORT)) + && (channelTypeValue != static_cast(::routing::ChannelType::CAN)) + && (channelTypeValue != static_cast(::routing::ChannelType::FR))) + { + return false; + } + + header.channelType = static_cast<::routing::ChannelType>(channelTypeValue); + header.channelId = channelId; + } + + return true; +} +} // namespace routing diff --git a/libs/bsw/routing/src/routing/LegacyTxAdapter.cpp b/libs/bsw/routing/src/routing/LegacyTxAdapter.cpp new file mode 100644 index 00000000000..54aa417f15f --- /dev/null +++ b/libs/bsw/routing/src/routing/LegacyTxAdapter.cpp @@ -0,0 +1,212 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/LegacyTxAdapter.h" + +#include "routing/Logger.h" + +#include +#include +#include +#include +#include + +namespace routing +{ + +namespace logger = ::util::logger; + +// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg): Logger API uses C-style varargs. + +LegacyTxAdapter::LegacyTxAdapter( + ::io::IWriter& outputWriter, TxAdapterTable const& table, ErrorHandler const errorHandler) +: _outputWriter(outputWriter) +, _table(table) +, _errorHandler(errorHandler) +, _buffer() +, _pduCounter() +, _byteCounter() +, _oversizedPdus() +, _failedMemAllocPdus() +, _unknownMessagePdus() +, _failedMemMovePdus() +, _droppedPduCounter() +, _droppedByteCounter() +, _lastAllocationFailed(false) +{} + +size_t LegacyTxAdapter::maxSize() const { return _outputWriter.maxSize(); } + +::etl::span LegacyTxAdapter::allocate(size_t size) +{ + _buffer = {}; + + if (size > _outputWriter.maxSize()) + { + ++_oversizedPdus; + + ++_droppedPduCounter; + _droppedByteCounter += static_cast(size); + + return {}; + } + + // Check if we can allocate the max underlying size. + // This is because otherwise we might not have enough space to reallocate with offset and length + // in commit, but at this stage we do not know what offset and length will be. + auto mem = _outputWriter.allocate(_outputWriter.maxSize()); + + if (mem.size() != _outputWriter.maxSize()) + { + if (!_lastAllocationFailed) + { + _errorHandler(ErrorHandler::StatusCode::MEM_ALLOCATION_FAILURE); + _lastAllocationFailed = true; + } + + ++_failedMemAllocPdus; + + ++_droppedPduCounter; + _droppedByteCounter += static_cast(size); + + return {}; + } + + _buffer = mem.first(size); + _lastAllocationFailed = false; + return _buffer; +} + +void LegacyTxAdapter::commit() +{ + if (_buffer.empty()) + { + return; + } + + auto const bufferSize = _buffer.size(); + auto buffer = _buffer; + _buffer = {}; + + if (bufferSize < 2 * sizeof(::etl::be_uint32_t)) + { + logger::Logger::error( + logger::ROUTING, + "Buffer size %u is too small to contain message ID and length", + static_cast(bufferSize)); + + ++_droppedPduCounter; + _droppedByteCounter += static_cast(bufferSize); + + return; + } + + auto const messageId = buffer.take<::etl::be_uint32_t>(); + auto const* const messageIdPos + = ::etl::find(_table.messageIds.begin(), _table.messageIds.end(), messageId); + + if (messageIdPos == _table.messageIds.end()) + { + _errorHandler(ErrorHandler::StatusCode::UNKNOWN_MESSAGE_ID); + + logger::Logger::error( + logger::ROUTING, + "Attempting to output unknown message %u", + static_cast(messageId)); + + ++_unknownMessagePdus; + + ++_droppedPduCounter; + _droppedByteCounter += static_cast(bufferSize); + + return; + } + + size_t const index + = static_cast(::etl::distance(_table.messageIds.begin(), messageIdPos)); + uint32_t const messageLength = _table.messageLengths[index]; + uint32_t const pduOffset = _table.pduOffsets[index]; + + // length + (void)buffer.take<::etl::be_uint32_t const>(); + + // space for ID + size + payload of message + size_t const messageBufferSize = messageLength + 2 * sizeof(::etl::be_uint32_t); + auto out = _outputWriter.allocate(messageBufferSize); + // Since _outputWriter.maxSize() bytes are available in the queue, the allocation fails only if + // messageBufferSize is greater than that, i.e., it is an invalid entry in the table. + if (out.size() != messageBufferSize) + { + // The last memory allocation did not fail because _buffer was not empty. + _errorHandler(ErrorHandler::StatusCode::MEM_ALLOCATION_FAILURE); + _lastAllocationFailed = true; + + logger::Logger::error( + logger::ROUTING, + "Failed to allocate output message (ID=%u,length=%u)", + static_cast(messageId), + static_cast(messageLength)); + + ++_failedMemAllocPdus; + + ++_droppedPduCounter; + _droppedByteCounter += static_cast(messageBufferSize); + + return; + } + + if (pduOffset >= messageLength) + { + logger::Logger::error( + logger::ROUTING, + "Invalid PDU offset (ID=%u,msg-length=%u,offset=%u,buf-size=%u)", + static_cast(messageId), + static_cast(messageLength), + static_cast(pduOffset), + static_cast(buffer.size())); + + ++_failedMemMovePdus; + + ++_droppedPduCounter; + _droppedByteCounter += static_cast(messageBufferSize); + + return; + } + + out.take<::etl::be_uint32_t>() = messageId; + out.take<::etl::be_uint32_t>() = static_cast(messageLength); + + auto const payloadLength + = ::etl::min(out.size() - static_cast(pduOffset), buffer.size()); + if (pduOffset != 0) + { + // The buffer points to the same memory, so first move it with memmove which handles + // overlap, then fill the the gap with 0xFF. + (void)::etl::mem_move(buffer.begin(), payloadLength, out.subspan(pduOffset).begin()); + (void)::etl::mem_set(out.first(pduOffset).begin(), pduOffset, static_cast(0xFF)); + } + + // This needs to be done whether there is an offset or not. + out.advance(pduOffset + payloadLength); + (void)::etl::mem_set(out.begin(), out.size(), static_cast(0xFF)); + + _outputWriter.commit(); + + ++_pduCounter; + _byteCounter += static_cast(messageBufferSize); + + return; +} + +void LegacyTxAdapter::flush() { _outputWriter.flush(); } + +// NOLINTEND(cppcoreguidelines-pro-type-vararg) + +} // namespace routing diff --git a/libs/bsw/routing/src/routing/Logger.cpp b/libs/bsw/routing/src/routing/Logger.cpp new file mode 100644 index 00000000000..8b47fbe8e04 --- /dev/null +++ b/libs/bsw/routing/src/routing/Logger.cpp @@ -0,0 +1,12 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/Logger.h" +DEFINE_LOGGER_COMPONENT(ROUTING) diff --git a/libs/bsw/routing/src/routing/PduTransportBufferedWriter.cpp b/libs/bsw/routing/src/routing/PduTransportBufferedWriter.cpp new file mode 100644 index 00000000000..52f27011551 --- /dev/null +++ b/libs/bsw/routing/src/routing/PduTransportBufferedWriter.cpp @@ -0,0 +1,119 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/PduTransportBufferedWriter.h" + +#include +#include + +#include + +namespace routing +{ + +::etl::span PduTransportBufferedWriter::allocate(size_t const size) +{ + size_t const MAXIMUM_BUFFER_SIZE = _destination.maxSize(); + if (size > MAXIMUM_BUFFER_SIZE) + { + _currentPduSize = 0; + return {}; + } + + bool const isEnoughSpaceAvailable = _bufferRemainder.size() >= size; + if (isEnoughSpaceAvailable) + { + _currentPduSize = size; + return _bufferRemainder.first(_currentPduSize); + } + + // If some data has already been committed, the buffer must be flushed prior to allocation. + if (!_isBufferEmpty) + { + flush(); + } + + _bufferRemainder = _destination.allocate(MAXIMUM_BUFFER_SIZE); + if (_bufferRemainder.size() != MAXIMUM_BUFFER_SIZE) + { + _currentPduSize = 0; + return {}; + } + + _currentPduSize = size; + return _bufferRemainder.first(_currentPduSize); +} + +void PduTransportBufferedWriter::commit() +{ + if (_currentPduSize == 0) + { + return; + } + + if (_bufferRemainder.size() == _destination.maxSize()) + { + // First chunk + _timestamp = getSystemTimeUs32Bit(); + _isBufferEmpty = false; + } + + _bufferRemainder.advance(_currentPduSize); + _currentPduSize = 0; + + if (_transmissionTimeout == 0) + { + ++_timeoutSends; + flush(); + } +} + +void PduTransportBufferedWriter::flush() +{ + if (_isBufferEmpty) + { + return; + } + + if (!_bufferRemainder.empty()) + { + // Trim the buffer before committing. + (void)_destination.allocate(_destination.maxSize() - _bufferRemainder.size()); + } + ++_flushes; + _destination.commit(); + + reset(); +} + +void PduTransportBufferedWriter::checkTransmissionTimeout() +{ + // If transmission timeout is equal to zero, the buffer is always empty after commit. + if (_isBufferEmpty) + { + return; + } + + // Transmission timeout is greater than zero and a multiple of 1000. + if ((getSystemTimeUs32Bit() - _timestamp) > (_transmissionTimeout - 1000U)) + { + ++_timeoutSends; + flush(); + } +} + +void PduTransportBufferedWriter::reset() +{ + _bufferRemainder = {}; + _isBufferEmpty = true; + _currentPduSize = 0; +} + +} // namespace routing diff --git a/libs/bsw/routing/src/routing/PduTransportConfig.cpp b/libs/bsw/routing/src/routing/PduTransportConfig.cpp new file mode 100644 index 00000000000..927c6e70d74 --- /dev/null +++ b/libs/bsw/routing/src/routing/PduTransportConfig.cpp @@ -0,0 +1,98 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/PduTransportConfig.h" + +#include + +namespace routing +{ +bool load(::etl::span mem, PduTransportConfig& config) +{ + if (mem.empty()) + { + return false; + } + + Header header; + (void)load(mem, header); + if (header.configType != ::blob::Config::Type::CHANNEL) + { + return false; + } + if (header.channelType != ::routing::ChannelType::PDU_TRANSPORT) + { + return false; + } + config.channelId = header.channelId; + + if (mem.size() < sizeof(config.type) + sizeof(config.mode) + sizeof(config.vlanId) + + sizeof(config.localPort) + sizeof(config.remotePort) + + sizeof(::etl::be_uint16_t)) + { + return false; + } + + config.type = mem.take(); + + auto const modeValue = mem.take(); + if ((modeValue != static_cast(PduTransportConfig::Mode::RX)) + && (modeValue != static_cast(PduTransportConfig::Mode::TX)) + && (modeValue != static_cast(PduTransportConfig::Mode::RX_TX))) + { + return false; + } + config.mode = static_cast(modeValue); + config.vlanId = mem.take<::etl::be_uint16_t const>(); + config.localPort = mem.take<::etl::be_uint16_t const>(); + config.remotePort = mem.take<::etl::be_uint16_t const>(); + + uint16_t const ipVersionValue = mem.take<::etl::be_uint16_t const>(); + if ((ipVersionValue != static_cast(::ip::IPAddress::Family::IPV4)) + && (ipVersionValue != static_cast(::ip::IPAddress::Family::IPV6))) + { + return false; + } + auto const ipVersion = static_cast<::ip::IPAddress::Family>(ipVersionValue); + bool const isIpV4 = ipVersion == ::ip::IPAddress::Family::IPV4; +#ifdef ESR_NO_IPV6 + if (!isIpV4 || (mem.size() < ::ip::IPAddress::IP4LENGTH)) + { + return false; + } + auto const ipAddressLength = ::ip::IPAddress::IP4LENGTH; + config.ipAddress = ::ip::make_ip4(mem.take(ipAddressLength)); +#else + auto const ipAddressLength = isIpV4 ? ::ip::IPAddress::IP4LENGTH : ::ip::IPAddress::IP6LENGTH; + if (mem.size() < ipAddressLength) + { + return false; + } + config.ipAddress = isIpV4 ? ::ip::make_ip4(mem.take(ipAddressLength)) + : ::ip::make_ip6(mem.take(ipAddressLength)); +#endif + + if (mem.size() < sizeof(config.pcp) + sizeof(config.transmissionTimeout) + sizeof(uint8_t)) + { + return false; + } + + config.pcp = mem.take(); + config.transmissionTimeout = mem.take<::etl::be_uint16_t const>(); + auto const remoteIpAddressCount = static_cast(mem.take()); + if (mem.size() < ipAddressLength * remoteIpAddressCount) + { + return false; + } + config.remoteIpAddresses = mem.take(ipAddressLength * remoteIpAddressCount); + + return true; +} +} // namespace routing diff --git a/libs/bsw/routing/src/routing/PduTransportTxAdapter.cpp b/libs/bsw/routing/src/routing/PduTransportTxAdapter.cpp new file mode 100644 index 00000000000..999e2a9fd83 --- /dev/null +++ b/libs/bsw/routing/src/routing/PduTransportTxAdapter.cpp @@ -0,0 +1,76 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/PduTransportTxAdapter.h" + +namespace routing +{ + +PduTransportTxAdapter::PduTransportTxAdapter( + ::io::IWriter& outputWriter, ErrorHandler const errorHandler) +: _outputWriter(outputWriter) +, _errorHandler(errorHandler) +, _currentPduSize(0U) +, _pduCounter() +, _byteCounter() +, _failedMemAllocPdus() +, _droppedPduCounter() +, _droppedByteCounter() +, _lastAllocationFailed(false) +{} + +size_t PduTransportTxAdapter::maxSize() const { return _outputWriter.maxSize(); } + +::etl::span PduTransportTxAdapter::allocate(size_t size) +{ + auto const buffer = _outputWriter.allocate(size); + if (buffer.size() != size) + { + _currentPduSize = 0U; + + if (!_lastAllocationFailed) + { + _errorHandler(ErrorHandler::StatusCode::MEM_ALLOCATION_FAILURE); + _lastAllocationFailed = true; + } + + ++_failedMemAllocPdus; + + ++_droppedPduCounter; + _droppedByteCounter += static_cast(size); + + return {}; + } + + _currentPduSize = buffer.size(); + _lastAllocationFailed = false; + return buffer; +} + +void PduTransportTxAdapter::commit() +{ + if (_currentPduSize == 0U) + { + return; + } + + _outputWriter.commit(); + + ++_pduCounter; + _byteCounter += static_cast(_currentPduSize); + + _currentPduSize = 0U; + + return; +} + +void PduTransportTxAdapter::flush() { _outputWriter.flush(); } + +} // namespace routing diff --git a/libs/bsw/routing/src/routing/RxAdapter.cpp b/libs/bsw/routing/src/routing/RxAdapter.cpp new file mode 100644 index 00000000000..a462ec933ee --- /dev/null +++ b/libs/bsw/routing/src/routing/RxAdapter.cpp @@ -0,0 +1,193 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/RxAdapter.h" + +#include "routing/Logger.h" + +#include +#include +#include +#include +#include + +namespace routing +{ +// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg): Logger API uses C-style varargs. +RxAdapter::RxAdapter( + ::io::IReader& inputReader, + ::etl::span const buffer, + RxAdapterTable const& table, + ErrorHandler const errorHandler) +: _inputReader(inputReader) +, _buffer(buffer) +, _table(table) +, _errorHandler(errorHandler) +, _currentMessageIndex(0U) +, _currentPduIndex(0U) +, _currentPdu() +, _pduCounter() +, _byteCounter() +, _invalidHeaderSizePdus() +, _unknownMessagePdus() +, _invalidParsedPayloadLengthPdus() +, _invalidPayloadLengthPdus() +, _discardedPduCounter() +, _discardedByteCounter() +{} + +size_t RxAdapter::maxSize() const { return _buffer.size(); } + +ErrorHandler::StatusCode RxAdapter::lookup(::etl::span message) const +{ + if (message.size() < MESSAGE_HEADER_SIZE) + { + ++_invalidHeaderSizePdus; + + return ErrorHandler::StatusCode::INVALID_MESSAGE_HEADER_SIZE; + } + + auto const messageId = message.take<::etl::be_uint32_t const>(); + auto const parsedPayloadLength = message.take<::etl::be_uint32_t const>(); + + if (parsedPayloadLength > message.size()) + { + ++_invalidParsedPayloadLengthPdus; + + return ErrorHandler::StatusCode::INVALID_PARSED_LENGTH; + } + + auto const* const pos + = ::etl::find(_table.messageIds.begin(), _table.messageIds.end(), messageId); + if (pos == _table.messageIds.end()) + { + ++_unknownMessagePdus; + + return ErrorHandler::StatusCode::UNKNOWN_MESSAGE_ID; + } + + _currentMessageIndex = static_cast(::etl::distance(_table.messageIds.begin(), pos)); + + return ErrorHandler::StatusCode::OK; +} + +// Return whether a PDU was extracted or not +bool RxAdapter::extract( + ::etl::span message, + size_t const channelSpecificPduId, + ::etl::span& pdu) const +{ + auto messagePayload = message.subspan(MESSAGE_HEADER_SIZE); + size_t const pduOffset = _table.pduOffsets[channelSpecificPduId]; + size_t const pduLength = _table.pduLengths[channelSpecificPduId]; + + if ((pduOffset + pduLength) > messagePayload.size()) + { + return false; + } + + messagePayload.advance(pduOffset); + auto const payload = messagePayload.take(pduLength); + + if ((MESSAGE_HEADER_SIZE + payload.size()) > pdu.size()) + { + return false; + } + + pdu = pdu.first(MESSAGE_HEADER_SIZE + payload.size()); + auto mem = pdu; + mem.take<::etl::be_uint32_t>() = static_cast(_table.firstId + channelSpecificPduId); + mem.take<::etl::be_uint32_t>() = static_cast(payload.size()); + + return ::etl::copy(payload, mem); +} + +::etl::span RxAdapter::peek() const +{ + // Return the last PDU extracted. + if (!_currentPdu.empty()) + { + return _currentPdu; + } + + ::etl::span message = _inputReader.peek(); + while (!message.empty()) + { + bool const isNewMessage = _currentPduIndex == 0; + if (isNewMessage) + { + ++_pduCounter; + _byteCounter += static_cast(message.size()); + + auto const statusCode = lookup(message); + if (statusCode != ErrorHandler::StatusCode::OK) + { + _errorHandler(statusCode, message); + + ++_discardedPduCounter; + _discardedByteCounter += static_cast(message.size()); + + _inputReader.release(); + message = _inputReader.peek(); + + continue; + } + } + + bool noPduExtracted = true; + auto const numberOfPdus = _table.pduLengthsOffsets[_currentMessageIndex + 1] + - _table.pduLengthsOffsets[_currentMessageIndex]; + while (noPduExtracted && (_currentPduIndex < numberOfPdus)) + { + auto const channelSpecificPduId + = _table.pduLengthsOffsets[_currentMessageIndex] + _currentPduIndex; + auto pdu = _buffer; + if (extract(message, channelSpecificPduId, pdu)) + { + _currentPdu = pdu; + noPduExtracted = false; + } + + ++_currentPduIndex; + } + + if (_currentPduIndex == numberOfPdus) + { + _inputReader.release(); + _currentPduIndex = 0; + } + + if (noPduExtracted) + { + if (isNewMessage) + { + // Although this message was expected, no PDU was extracted from it because all the + // expected PDUs were too big + _errorHandler(ErrorHandler::StatusCode::INVALID_PAYLOAD_LENGTH, message); + + ++_invalidPayloadLengthPdus; + + ++_discardedPduCounter; + _discardedByteCounter += static_cast(message.size()); + } + message = _inputReader.peek(); + + continue; + } + + return _currentPdu; + } + return {}; +} + +void RxAdapter::release() { _currentPdu = {}; } + +// NOLINTEND(cppcoreguidelines-pro-type-vararg) +} // namespace routing diff --git a/libs/bsw/routing/src/routing/RxAdapterTable.cpp b/libs/bsw/routing/src/routing/RxAdapterTable.cpp new file mode 100644 index 00000000000..2d03857d386 --- /dev/null +++ b/libs/bsw/routing/src/routing/RxAdapterTable.cpp @@ -0,0 +1,55 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/RxAdapterTable.h" + +#include "routing/Header.h" +#include "routing/util.h" + +#include +#include + +namespace routing +{ + +bool load(::etl::span mem, RxAdapterTable& table) +{ + if (mem.empty()) + { + return false; + } + + ::routing::Header header; + if (!::routing::load(mem, header)) + { + return false; + } + + if (header.configType != ::blob::Config::Type::RX_ADAPTER) + { + return false; + } + + if (mem.size() < sizeof(table.firstId)) + { + return false; + } + + table.firstId = mem.take<::etl::be_uint32_t const>(); + mem = ::blob::loadColumn(table.messageIds, mem); + mem = ::blob::loadColumn(table.messageLengths, mem); + mem = ::blob::loadColumn(table.pduLengthsOffsets, mem); + mem = ::blob::loadColumn(table.pduLengths, mem); + mem = ::blob::loadColumn(table.pduOffsets, mem); + + return true; +} + +} // namespace routing diff --git a/libs/bsw/routing/src/routing/TxAdapterTable.cpp b/libs/bsw/routing/src/routing/TxAdapterTable.cpp new file mode 100644 index 00000000000..6f78a5e8c8d --- /dev/null +++ b/libs/bsw/routing/src/routing/TxAdapterTable.cpp @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/TxAdapterTable.h" + +#include "routing/Header.h" + +#include +#include + +namespace routing +{ + +bool load(::etl::span mem, TxAdapterTable& table) +{ + if (mem.empty()) + { + return false; + } + + ::routing::Header header; + if (!::routing::load(mem, header)) + { + return false; + } + + if (header.configType != ::blob::Config::Type::TX_ADAPTER) + { + return false; + } + + mem = ::blob::loadColumn(table.messageIds, mem); + mem = ::blob::loadColumn(table.messageLengths, mem); + mem = ::blob::loadColumn(table.pduOffsets, mem); + + return true; +} + +} // namespace routing diff --git a/libs/bsw/routing/src/routing/pduRouting.cpp b/libs/bsw/routing/src/routing/pduRouting.cpp new file mode 100644 index 00000000000..26e252e5601 --- /dev/null +++ b/libs/bsw/routing/src/routing/pduRouting.cpp @@ -0,0 +1,138 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/pduRouting.h" + +#include "routing/Logger.h" + +#include +#include +#include +#include + +namespace routing +{ +namespace logger = ::util::logger; + +// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg): Logger API uses C-style varargs. + +bool load(::etl::span mem, ::routing::PduRoutingTable& table) +{ + if (mem.empty()) + { + return false; + } + + auto const header = ::blob::loadConfigHeader(mem); + if (header.configType != ::blob::Config::Type::ROUTING) + { + return false; + } + + mem = ::blob::loadColumn(table.destinationOffsets, mem); + mem = ::blob::loadColumn(table.outputMessageIds, mem); + mem = ::blob::loadColumn(table.destinations, mem); + + return true; +} + +bool route( + uint8_t const srcChannelId, + PduRoutingTable const& table, + ::io::IReader& reader, + ::etl::span<::io::IWriter*> const writers) +{ + auto const pdu = reader.peek(); + + if (pdu.empty()) + { + return false; + } + + // We need to be able to extract at least the internalPduId + if (pdu.size() < sizeof(::etl::be_uint32_t)) + { + reader.release(); + logger::Logger::error( + logger::ROUTING, "Invalid PDU size [%u]", static_cast(pdu.size())); + return false; + } + + logger::Logger::debug( + logger::ROUTING, + "Processing input PDU (src-channel-index: %u, pdu-size: %u)", + srcChannelId, + static_cast(pdu.size())); + + uint32_t const internalPduId = ::etl::be_uint32_t(pdu.data()); + if (static_cast(internalPduId) + 1U >= table.destinationOffsets.size()) + { + reader.release(); + logger::Logger::error( + logger::ROUTING, "Unknown PDU [%u]", static_cast(internalPduId)); + return false; + } + + logger::Logger::debug( + logger::ROUTING, + "Looking for a route (internal-pdu-id: %u)", + static_cast(internalPduId)); + + uint32_t const start = table.destinationOffsets[internalPduId]; + uint32_t const end = table.destinationOffsets[static_cast(internalPduId) + 1U]; + + auto const dsts = table.destinations.subspan(start, end - start); + auto const outputMessageIds = table.outputMessageIds.subspan(start, end - start); + + auto const payload = pdu.subspan(sizeof(::etl::be_uint32_t)); + for (size_t i = 0; i < dsts.size(); i++) + { + auto const dst = dsts[i]; + logger::Logger::debug(logger::ROUTING, "Routing PDU to destination [%u]", dst); + auto* const writer = dst < writers.size() ? writers[dst] : nullptr; + if (writer == nullptr) + { + continue; + } + ::routing::outputPdu(outputMessageIds[i], dst, payload, *writer); + } + + reader.release(); + + return true; +} + +void outputPdu( + ::etl::be_uint32_t const outputMessageId, + uint8_t const dstChannelId, + ::etl::span const payload, + ::io::IWriter& writer) +{ + auto const pduSize = sizeof(::etl::be_uint32_t) + payload.size(); + auto dstPdu = writer.allocate(pduSize); + if (dstPdu.size() != pduSize) + { + return; + } + + dstPdu.take<::etl::be_uint32_t>() = outputMessageId; + (void)::etl::copy(payload, dstPdu); + writer.commit(); + + logger::Logger::debug( + logger::ROUTING, + "Writing PDU to output (msg-id: %u, dst-channel-index: %u, payload-size: %u)", + static_cast(outputMessageId), + dstChannelId, + static_cast(payload.size())); +} + +// NOLINTEND(cppcoreguidelines-pro-type-vararg) +} // namespace routing diff --git a/libs/bsw/routing/src/routing/util.cpp b/libs/bsw/routing/src/routing/util.cpp new file mode 100644 index 00000000000..c95684a6498 --- /dev/null +++ b/libs/bsw/routing/src/routing/util.cpp @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/util.h" + +#include +#include + +namespace routing +{ +::blob::Config config( + ::etl::span const blob, ::blob::Config::Type const type, uint8_t const channelId) +{ + for (auto const config : ::blob::Blob(blob)) + { + if (config.type != type) + { + continue; + } + + auto data = config.data; + ::routing::Header header; + if (!::routing::load(data, header)) + { + continue; + } + + if (header.channelId != channelId) + { + continue; + } + + return config; + } + + return {}; +} + +} // namespace routing diff --git a/libs/bsw/routing/test/CMakeLists.txt b/libs/bsw/routing/test/CMakeLists.txt new file mode 100644 index 00000000000..9646e18735a --- /dev/null +++ b/libs/bsw/routing/test/CMakeLists.txt @@ -0,0 +1,41 @@ +# How to generate the test blob and its header files: +# python -m blob binary -i routing.jsonl -c blob.routing.table | python -m blob +# header data -n CONFIGURATION_BLOB > include/blob/configuration.h +# python -m blob.routing header ${BLOB_INPUT_FILE} > include/routing/channelId.h +# python -m blob header config-type > include/blob/ConfigType.h + +target_include_directories(routingConfiguration INTERFACE include) + +add_executable( + routingTest + src/routing/definition.cpp + src/io/udp/SenderTest.cpp + src/io/udp/ReceiverTest.cpp + src/routing/PduTransportBufferedWriterTest.cpp + src/routing/ChannelNamesTest.cpp + src/routing/PduTransportIntegrationTest.cpp + src/routing/RxAdapterTableTest.cpp + src/routing/TxAdapterTableTest.cpp + src/routing/PduRoutingTableTest.cpp + src/routing/PduTransportRxAdapterTest.cpp + src/routing/ClampedCounterTest.cpp + src/routing/IntegrationTest.cpp + src/routing/LegacyRxAdapterTest.cpp + src/routing/RxAdapterTest.cpp + src/routing/PduTransportConfigTest.cpp + src/routing/HeaderTest.cpp + src/routing/RouterTest.cpp + src/routing/LegacyTxAdapterTest.cpp + src/routing/PduTransportTxAdapterTest.cpp) + +target_include_directories(routingTest PUBLIC include) + +target_link_libraries( + routingTest + PRIVATE routing + ioMock + cpp2ethernetMock + bspMock + gmock_main) + +gtest_discover_tests(routingTest PROPERTIES LABELS "routingTest") diff --git a/libs/bsw/routing/test/include/blob/ConfigType.h b/libs/bsw/routing/test/include/blob/ConfigType.h new file mode 100644 index 00000000000..9eafa48d8b3 --- /dev/null +++ b/libs/bsw/routing/test/include/blob/ConfigType.h @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// This is a generated file. Please do not edit it. + +#pragma once + +#include + +namespace blob +{ +enum class ConfigType : uint32_t +{ + ROUTING = 0x00, + RX_ADAPTER = 0x01, + TX_ADAPTER = 0x02, + CHANNEL = 0xCC, + CHANNEL_NAMES = 0xDD, + META = 0xFE, + UNKNOWN = 0xFFFFFFFF +}; +} // namespace blob diff --git a/libs/bsw/routing/test/include/blob/configuration.h b/libs/bsw/routing/test/include/blob/configuration.h new file mode 100644 index 00000000000..b4df1921064 --- /dev/null +++ b/libs/bsw/routing/test/include/blob/configuration.h @@ -0,0 +1,180 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// This is a generated file. Please do not edit it. + +#pragma once + +#include + +namespace blob +{ +uint8_t const CONFIGURATION_BLOB[2548] = { + 0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x09, 0xE8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x5C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x0B, + 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x15, + 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x00, 0x68, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x03, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x10, 0x92, + 0x00, 0x00, 0x10, 0x92, 0x00, 0x00, 0x10, 0x92, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x10, 0x03, + 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x16, + 0x00, 0x00, 0x01, 0x4D, 0x00, 0x00, 0x02, 0x2B, 0x00, 0x00, 0x00, 0x6F, 0x00, 0x00, 0x02, 0x9A, + 0x00, 0x00, 0x00, 0xDE, 0x00, 0x00, 0x03, 0xE7, 0x00, 0x00, 0x00, 0x1A, 0x03, 0x03, 0x03, 0x03, + 0x01, 0x04, 0x03, 0x03, 0x03, 0x03, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x03, + 0x05, 0x07, 0x03, 0x08, 0x04, 0x0B, 0xFF, 0xFF, 0x55, 0xAF, 0x22, 0x4A, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x05, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0xA3, 0x1B, 0x9D, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x1F, 0x16, 0x5C, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x01, 0x00, 0x00, 0x10, 0x92, 0xCC, 0xCC, 0xBB, 0xAA, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x40, + 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1A, 0xF7, 0x5C, 0xDC, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAC, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x12, 0xAC, 0xB9, 0x45, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x9A, 0xFF, 0xF2, 0x1A, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5B, 0xB8, 0xC0, 0xF3, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0xBC, 0x00, 0x00, 0x01, 0xBD, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x69, 0x6E, 0x9D, 0xD5, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xEA, 0xF2, 0x84, 0x2A, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x66, 0x40, 0x8B, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x09, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x7A, 0xA1, 0xF7, 0x6D, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x03, 0x78, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x85, 0xFC, 0x2F, 0xE0, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x16, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC8, 0x61, 0x59, 0xB6, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0x00, 0x00, 0x00, 0x14, + 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x14, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x8A, 0xD4, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xDB, 0xA6, 0x1C, 0xC7, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0xB8, 0xFD, 0xE5, 0x59, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, + 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x10, 0x92, + 0x00, 0x00, 0x10, 0x92, 0x00, 0x00, 0x10, 0x92, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x6F, + 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x40, + 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x28, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9D, 0x2B, 0xA3, 0xF5, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x10, 0x03, + 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x00, 0xDE, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xAB, 0xA1, 0x5E, 0xC6, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x4D, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x6D, 0x56, 0x22, 0x21, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x2C, 0xF2, 0xA8, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x2B, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x3E, 0xE4, 0x3E, 0xA3, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x02, 0x9A, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0xD6, 0x04, 0xE6, 0x8F, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1E, 0x39, 0x7D, 0x86, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x65, 0x27, 0xFF, 0x65, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0B, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0xE7, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x4B, 0x8C, 0x1A, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1C, 0x01, 0x11, 0x00, 0x00, 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, + 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x47, 0xE1, 0xE5, 0xB1, + 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + 0x01, 0x11, 0x00, 0x00, 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, 0xFF, 0xFD, 0x01, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0xFF, 0xFF, 0xC2, 0xFF, 0xA3, 0x5B, + 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, + 0x01, 0x10, 0x00, 0x00, 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, 0xFF, 0xFD, 0x00, 0x00, + 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0xFF, 0xFF, + 0xCB, 0xC0, 0xBD, 0x7D, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x01, 0x01, 0x00, 0x00, 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, + 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x49, 0x0F, 0xDC, 0xE7, 0x00, 0x00, 0x00, 0xCC, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x01, 0x10, 0x00, 0x00, + 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, 0xFF, 0xFD, 0x02, 0x00, 0x02, 0x00, 0xFF, 0xFF, + 0x09, 0x69, 0xD7, 0x11, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x01, 0x10, 0x00, 0x00, 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, + 0xFF, 0xFD, 0x00, 0x00, 0x03, 0x00, 0xFF, 0xFF, 0xA0, 0xA7, 0xDD, 0x60, 0x00, 0x00, 0x00, 0xCC, + 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x01, 0x01, 0x00, 0x00, + 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, 0xFF, 0xFD, 0x03, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0x93, 0x21, 0x62, 0x56, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x01, 0x01, 0x00, 0x00, 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, + 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xB0, 0xBD, 0x7B, 0x94, 0x00, 0x00, 0x00, 0xCC, + 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x01, 0x10, 0x00, 0x00, + 0x11, 0x5E, 0x11, 0x5E, 0x00, 0x04, 0xEF, 0xC0, 0xFF, 0xFD, 0x07, 0x00, 0x05, 0x00, 0xFF, 0xFF, + 0x3D, 0xC1, 0xD9, 0x68, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x0A, 0x01, 0x00, 0x00, 0x10, 0x92, 0x0A, 0x03, 0xFF, + 0x4E, 0x24, 0x61, 0xE8, 0x00, 0x00, 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xC0, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0D, 0x00, 0x12, 0x00, 0x21, + 0x00, 0x30, 0x00, 0x3F, 0x00, 0x4E, 0x00, 0x5D, 0x00, 0x6C, 0x00, 0x7B, 0x00, 0x8A, 0x00, 0xA0, + 0x43, 0x41, 0x4E, 0x30, 0x00, 0x46, 0x4C, 0x45, 0x58, 0x52, 0x41, 0x59, 0x00, 0x43, 0x41, 0x4E, + 0x31, 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, 0x30, + 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, 0x31, 0x00, + 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, 0x32, 0x00, 0x50, + 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, 0x33, 0x00, 0x50, 0x44, + 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, 0x34, 0x00, 0x50, 0x44, 0x55, + 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, 0x35, 0x00, 0x50, 0x44, 0x55, 0x5F, + 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, 0x36, 0x00, 0x50, 0x44, 0x55, 0x5F, 0x54, + 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, 0x37, 0x00, 0x55, 0x4E, 0x4E, 0x41, 0x4D, 0x45, + 0x44, 0x5F, 0x50, 0x44, 0x55, 0x5F, 0x54, 0x52, 0x41, 0x4E, 0x53, 0x50, 0x4F, 0x52, 0x54, 0x00, + 0x6A, 0xC5, 0x6F, 0x20}; +} // namespace blob diff --git a/libs/bsw/routing/test/include/routing/channelId.h b/libs/bsw/routing/test/include/routing/channelId.h new file mode 100644 index 00000000000..866692ca744 --- /dev/null +++ b/libs/bsw/routing/test/include/routing/channelId.h @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +namespace routing +{ +// clang-format off +static constexpr uint8_t CAN0 = 0; +static constexpr uint8_t FLEXRAY = 1; +static constexpr uint8_t CAN1 = 2; + +static constexpr uint8_t canChannelIds[] = {CAN0, CAN1}; + +static constexpr uint8_t frChannelIds[] = {FLEXRAY}; +// clang-format on +} // namespace routing diff --git a/libs/bsw/routing/test/include/routing/constants.h b/libs/bsw/routing/test/include/routing/constants.h new file mode 100644 index 00000000000..a6f2d7b3cb1 --- /dev/null +++ b/libs/bsw/routing/test/include/routing/constants.h @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/channelId.h" + +#include + +namespace routing +{ +static constexpr uint8_t NUM_PDU_TRANSPORT_CHANNELS = 9U; +static constexpr uint8_t NUM_CAN_CHANNELS = ::etl::make_span(::routing::canChannelIds).size(); +static constexpr uint8_t NUM_FLEXRAY_CHANNELS = ::etl::make_span(::routing::frChannelIds).size(); +static constexpr uint8_t NUM_CHANNELS + = NUM_CAN_CHANNELS + NUM_FLEXRAY_CHANNELS + NUM_PDU_TRANSPORT_CHANNELS; + +} // namespace routing diff --git a/libs/bsw/routing/test/include/routing/definition.h b/libs/bsw/routing/test/include/routing/definition.h new file mode 100644 index 00000000000..c279bdd9e95 --- /dev/null +++ b/libs/bsw/routing/test/include/routing/definition.h @@ -0,0 +1,136 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include "routing/RxAdapterTable.h" +#include "routing/TxAdapterTable.h" + +#include +#include +#include + +#include + +namespace routing +{ +struct PduRoutingTable; + +struct Definition +{ + static constexpr size_t MAX_OUTPUTS = 255; + + struct Pdu + { + Pdu(uint8_t const channelId, + uint32_t const messageId, + uint32_t const messageLength, + uint32_t const offset, + uint32_t const length) + : channelId(channelId) + , messageId(::etl::be_uint32_t(messageId)) + , messageLength(::etl::be_uint32_t(messageLength)) + , offset(offset) + , length(length){}; + + Pdu() = default; + + uint8_t channelId = 0; + ::etl::be_uint32_t messageId = 0U; + ::etl::be_uint32_t messageLength = 0U; + uint32_t offset = 0; + uint32_t length = 0; + }; + + Definition() = default; + + Definition& + in(uint8_t const channelId, + uint32_t const messageId, + uint32_t const offset, + uint32_t const length) + { + input = {channelId, messageId, 0, offset, length}; + return *this; + } + + Definition& + in(uint8_t const channelId, + uint32_t const messageId, + uint32_t const messageLength, + uint32_t const offset, + uint32_t const length) + { + input = {channelId, messageId, messageLength, offset, length}; + return *this; + } + + Definition& + out(uint8_t const channelId, + uint32_t const messageId, + uint32_t const messageLength, + uint32_t const offset) + { + outputs.push_back({channelId, messageId, messageLength, offset, 0}); + return *this; + } + + Pdu input; + ::etl::vector outputs; +}; + +struct DefinitionRef +{ + Definition const* d; +}; + +struct RoutingTableMem +{ + static constexpr size_t MAX_IDS = 1024; + ::etl::vector<::etl::be_uint32_t, MAX_IDS> destinationOffsets; + ::etl::vector<::etl::be_uint32_t, MAX_IDS> outputMessageIds; + ::etl::vector destinations; +}; + +struct RxAdapterTableMem +{ + static constexpr size_t MAX_IDS = 1024; + ::etl::vector<::etl::be_uint32_t, MAX_IDS> messageIds; + ::etl::vector<::etl::be_uint32_t, MAX_IDS> messageLengths; + ::etl::vector<::etl::be_uint32_t, MAX_IDS> pduLengthsOffsets; + ::etl::vector<::etl::be_uint32_t, MAX_IDS> pduLengths; + ::etl::vector<::etl::be_uint32_t, MAX_IDS> pduOffsets; +}; + +struct TxAdapterTableMem +{ + static constexpr size_t MAX_IDS = 1024; + ::etl::vector<::etl::be_uint32_t, MAX_IDS> messageIds; + ::etl::vector<::etl::be_uint32_t, MAX_IDS> messageLengths; + ::etl::vector<::etl::be_uint32_t, MAX_IDS> offsets; +}; + +void sort(::etl::span const definitions); + +void load( + TxAdapterTable& table, + ::etl::span input, + uint8_t const channelId, + TxAdapterTableMem& mem); + +void load( + RxAdapterTable& table, + ::etl::span input, + uint8_t const channelId, + RxAdapterTableMem& mem); + +void load(PduRoutingTable& table, ::etl::span input, RoutingTableMem& mem); + +} // namespace routing diff --git a/libs/bsw/routing/test/routing.jsonl b/libs/bsw/routing/test/routing.jsonl new file mode 100644 index 00000000000..c39d645c121 --- /dev/null +++ b/libs/bsw/routing/test/routing.jsonl @@ -0,0 +1,34 @@ +{"type": "channel", "value": {"name": "CAN0", "id": 0, "type": "can", "configuration": {}}} +{"type": "channel", "value": {"name": "CAN1", "id": 2, "type": "can", "configuration": {}}} +{"type": "channel", "value": {"name": "FLEXRAY", "id": 1, "type": "flexray", "configuration":{"flexray-fifo-depths":{"2561":10, "4242": 3}}}} +{"type": "channel", "value": {"name": "PDU_TRANSPORT0", "type": "pdu_transport", "configuration": {"connection-type": "multicast", "ip-address": "239.192.255.253", "mode": "rxtx", "local-port": 4446, "remote-port": 4446, "pcp": 0, "remote-ip-addresses": ["0.0.0.0"]}}} +{"type": "channel", "value": {"name": "PDU_TRANSPORT1", "type": "pdu_transport", "configuration": {"connection-type": "multicast", "ip-address": "239.192.255.253", "local-port": 4446, "remote-port": 4446, "pcp": 1, "remote-ip-addresses": ["0.0.0.0", "1.1.1.1"]}}} +{"type": "channel", "value": {"name": "PDU_TRANSPORT2", "type": "pdu_transport", "configuration": {"connection-type": "multicast", "ip-address": "239.192.255.253", "mode": "tx", "local-port": 4446, "remote-port": 4446, "pcp": 0, "transmission-timeout": 1, "remote-ip-addresses": ["0.0.0.0", "1.1.1.1", "2.2.2.2"]}}} +{"type": "channel", "value": {"name": "PDU_TRANSPORT3", "type": "pdu_transport", "configuration": {"connection-type": "multicast", "ip-address": "239.192.255.253", "mode": "rx", "local-port": 4446, "remote-port": 4446, "pcp": 0}}} +{"type": "channel", "value": {"name": "PDU_TRANSPORT4", "type": "pdu_transport", "configuration": {"connection-type": "multicast", "ip-address": "239.192.255.253", "mode": "tx", "local-port": 4446, "remote-port": 4446, "pcp": 2, "transmission-timeout": 2}}} +{"type": "channel", "value": {"name": "PDU_TRANSPORT5", "type": "pdu_transport", "configuration": {"connection-type": "multicast", "ip-address": "239.192.255.253", "mode": "tx", "local-port": 4446, "remote-port": 4446, "pcp": 0, "transmission-timeout": 3}}} +{"type": "channel", "value": {"name": "PDU_TRANSPORT6", "type": "pdu_transport", "configuration": {"connection-type": "multicast", "ip-address": "239.192.255.253", "mode": "rx", "local-port": 4446, "remote-port": 4446, "pcp": 3}}} +{"type": "channel", "value": {"name": "PDU_TRANSPORT7", "type": "pdu_transport", "configuration": {"connection-type": "multicast", "ip-address": "239.192.255.253", "mode": "rx", "local-port": 4446, "remote-port": 4446, "pcp": 0}}} +{"type": "channel", "value": {"name": "UNNAMED_PDU_TRANSPORT", "type": "pdu_transport", "configuration": {"connection-type": "multicast", "ip-address": "239.192.255.253", "mode": "tx", "local-port": 4446, "remote-port": 4446, "pcp": 7, "transmission-timeout": 5}}} +{"type":"routing","value":{"input":{"channel-name":"CAN0","message-id":1,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":256,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN0","message-id":2,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":257,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN0","message-id":2,"offset":0,"length":4},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":258,"offset":0,"length":4}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN0","message-id":2,"offset":4,"length":4},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":259,"offset":4,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN0","message-id":3,"offset":0,"length":8},"outputs":[{"channel-name":"FLEXRAY","message-id":768,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN0","message-id":4,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT1","message-id":16384,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"CAN0","message-id":257,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":2,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":17,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"CAN0","message-id":512,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":18,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"CAN0","message-id":1,"offset":0,"length":8},{"channel-name":"PDU_TRANSPORT1","message-id":4096,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":19,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT1","message-id":4098,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":20,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT1","message-id":4099,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":21,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT1","message-id":4100,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":257,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"CAN0","message-id":2,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT0","message-id":2,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"CAN0","message-id":257,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT1","message-id":20480,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"CAN0","message-id":5,"offset":0,"length":8},{"channel-name":"PDU_TRANSPORT0","message-id":22,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT3","message-id":444,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT2","message-id":333,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT3","message-id":445,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT4","message-id":555,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT6","message-id":777,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":111,"offset":0,"length":8},{"channel-name":"PDU_TRANSPORT5","message-id":666,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"PDU_TRANSPORT7","message-id":888,"message-length":8,"offset":0,"length":8},"outputs":[{"channel-name":"PDU_TRANSPORT1","message-id":222,"offset":0,"length":8},{"channel-name":"UNNAMED_PDU_TRANSPORT","message-id":999,"offset":0,"length":8}]}} +{"type":"routing","value":{"input":{"channel-name":"FLEXRAY","message-id":3435969450,"offset":0,"length":64},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":4242,"offset":0,"length":64}]}} +{"type":"routing","value":{"input":{"channel-name":"FLEXRAY","message-id":2561,"offset":0,"length":64},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":4242,"offset":0,"length":64}]}} +{"type":"routing","value":{"input":{"channel-name":"FLEXRAY","message-id":4242,"offset":0,"length":64},"outputs":[{"channel-name":"PDU_TRANSPORT0","message-id":4242,"offset":0,"length":64}]}} diff --git a/libs/bsw/routing/test/src/io/udp/ReceiverTest.cpp b/libs/bsw/routing/test/src/io/udp/ReceiverTest.cpp new file mode 100644 index 00000000000..00da06731ce --- /dev/null +++ b/libs/bsw/routing/test/src/io/udp/ReceiverTest.cpp @@ -0,0 +1,384 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "io/udp/Receiver.h" + +#include +#include +#include +#include + +#include + +using namespace ::testing; + +namespace +{ +struct ReceiverTest : Test +{ + static constexpr size_t NUMBER_OF_REMOTE_IP_ADDRESSES = 3; + static constexpr size_t MAX_ELEMENT_SIZE = 1416U; + static constexpr size_t CAPACITY = 10 * MAX_ELEMENT_SIZE; + using Queue = ::io::MemoryQueue; + using Reader = ::io::MemoryQueueReader; + using Writer = ::io::MemoryQueueWriter; + + ReceiverTest() + : socketMock() + , queue() + , reader(queue) + , writer(queue) + , errorHandler( + ::routing::ErrorHandler::Function::create( + *this), + 0U) + , remoteIpAddresses( + {::ip::make_ip4(0, 0, 0, 0), ::ip::make_ip4(1, 1, 1, 1), ::ip::make_ip4(2, 2, 2, 2)}) + , receiver( + socketMock, + writer, + errorHandler, + ::etl::make_span(remoteIpAddresses).reinterpret_as()) + , payload() + , bytesRead(0) + , invalidRemoteIpAddressCounter(0) + , memAllocationFailureCounter(0) + { + ON_CALL(socketMock, read(_, _)) + .WillByDefault( + [this](uint8_t* buffer, size_t n) -> size_t + { + (void)read(buffer, n); + return bytesRead; + }); + } + + void simulateUdpPacketReception( + ::etl::span const udpPayload, + ::ip::IPAddress const sourceAddress = ::ip::make_ip4(0, 0, 0, 0)) + { + ASSERT_LE(udpPayload.size(), 0xFFFF); + payload = udpPayload; + auto destinationAddress = ::ip::make_ip4(127, 0, 0, 1); + uint16_t port = 2222U; + socketMock.getDataListener()->dataReceived( + socketMock, sourceAddress, port, destinationAddress, udpPayload.size()); + } + + void read(uint8_t* buffer, size_t n) + { + if (buffer == nullptr || n == 0) + { + bytesRead = 0; + return; + } + + auto toBeRead = payload; + if (n != toBeRead.size()) + { + toBeRead = toBeRead.first(n); + } + ASSERT_TRUE(::etl::copy(toBeRead, ::etl::span(buffer, n))); + payload = payload.subspan(toBeRead.size()); + bytesRead = toBeRead.size(); + } + + void handleError(::routing::ErrorHandler::StatusCode const statusCode, uint8_t, uint32_t) + { + switch (statusCode) + { + case ::routing::ErrorHandler::StatusCode::INVALID_REMOTE_IP_ADDRESS: + ++invalidRemoteIpAddressCounter; + break; + case ::routing::ErrorHandler::StatusCode::MEM_ALLOCATION_FAILURE: + ++memAllocationFailureCounter; + default: break; + } + } + + NiceMock<::udp::AbstractDatagramSocketMock> socketMock; + + Queue queue; + Reader reader; + Writer writer; + + ::routing::ErrorHandler errorHandler; + ::etl::array<::ip::IPAddress, NUMBER_OF_REMOTE_IP_ADDRESSES> remoteIpAddresses; + ::io::udp::Receiver receiver; + + ::etl::span payload; + size_t bytesRead; + uint32_t invalidRemoteIpAddressCounter; + uint32_t memAllocationFailureCounter; +}; + +/** + * \desc: Check that reception of a UDP packet with a single PDU works. + */ +TEST_F(ReceiverTest, receive_udp_packet_with_single_pdu) +{ + uint8_t const udpPayload[] = { + 0x00, + 0x00, + 0x00, + 0x02, // PDU transport ID + 0x00, + 0x00, + 0x00, + 0x02, // size + 0xff, + 0xaa // data + }; + + auto element = reader.peek(); + ASSERT_EQ(0, element.size()); + + simulateUdpPacketReception(udpPayload); + + element = reader.peek(); + EXPECT_EQ(10, element.size()); + + reader.release(); + element = reader.peek(); + EXPECT_EQ(0, element.size()); + EXPECT_EQ(0, receiver.invalidIpAddressPdus()); + EXPECT_EQ(0, receiver.failedMemAllocPdus()); +} + +/** + * \desc: Check that reception of a UDP packet with multiple PDU works. + */ +TEST_F(ReceiverTest, receive_udp_packet_with_multiple_pdus) +{ + uint8_t const udpPayload[] = { + 0x00, 0x00, 0x00, 0x02, // PDU transport ID + 0x00, 0x00, 0x00, 0x02, // size + 0xff, 0xaa, // data + 0x00, 0x00, 0x00, 0x02, // PDU transport ID + 0x00, 0x00, 0x00, 0x02, // size + 0xff, 0xaa // data + }; + + auto element = reader.peek(); + ASSERT_EQ(0, element.size()); + + simulateUdpPacketReception(udpPayload); + + element = reader.peek(); + EXPECT_EQ(20, element.size()); + + reader.release(); + element = reader.peek(); + EXPECT_EQ(0, element.size()); + EXPECT_EQ(0, receiver.invalidIpAddressPdus()); + EXPECT_EQ(0, receiver.failedMemAllocPdus()); +} + +/** + * \desc: Check that reception of UDP packets from different sources works. + */ +TEST_F(ReceiverTest, receive_udp_packet_from_different_sources) +{ + uint8_t const udpPayload[] = { + 0x00, + 0x00, + 0x00, + 0x02, // PDU transport ID + 0x00, + 0x00, + 0x00, + 0x02, // size + 0xff, + 0xaa // data + }; + + auto element = reader.peek(); + ASSERT_EQ(0, element.size()); + + simulateUdpPacketReception(udpPayload, ::ip::make_ip4(1, 1, 1, 1)); + + element = reader.peek(); + EXPECT_EQ(10, element.size()); + + reader.release(); + element = reader.peek(); + EXPECT_EQ(0, element.size()); + + simulateUdpPacketReception(udpPayload, ::ip::make_ip4(2, 2, 2, 2)); + + element = reader.peek(); + EXPECT_EQ(10, element.size()); + + reader.release(); + element = reader.peek(); + EXPECT_EQ(0, element.size()); + EXPECT_EQ(0, receiver.invalidIpAddressPdus()); + EXPECT_EQ(0, receiver.failedMemAllocPdus()); +} + +/** + * \desc: Check that reception of a UDP packet with no remote IP address works. + */ +TEST_F(ReceiverTest, receive_udp_packet_no_remote_ip_addresses) +{ + new (&receiver)::io::udp::Receiver(socketMock, writer, errorHandler); + + uint8_t const udpPayload[] = { + 0x00, + 0x00, + 0x00, + 0x02, // PDU transport ID + 0x00, + 0x00, + 0x00, + 0x02, // size + 0xff, + 0xaa // data + }; + + auto element = reader.peek(); + ASSERT_EQ(0, element.size()); + + simulateUdpPacketReception(udpPayload, ::ip::make_ip4(3, 3, 3, 3)); + + element = reader.peek(); + EXPECT_EQ(10, element.size()); + + reader.release(); + element = reader.peek(); + EXPECT_EQ(0, element.size()); + EXPECT_EQ(0, receiver.invalidIpAddressPdus()); + EXPECT_EQ(0, receiver.failedMemAllocPdus()); +} + +/** + * \desc: Check that reception of a UDP packet from an invalid source fails. + */ +TEST_F(ReceiverTest, receive_udp_packet_from_invalid_source) +{ + uint8_t const udpPayload[] = { + 0x00, + 0x00, + 0x00, + 0x02, // PDU transport ID + 0x00, + 0x00, + 0x00, + 0x02, // size + 0xff, + 0xaa // data + }; + + auto element = reader.peek(); + ASSERT_EQ(0, element.size()); + + simulateUdpPacketReception(udpPayload, ::ip::make_ip4(3, 3, 3, 3)); + + element = reader.peek(); + EXPECT_EQ(0, element.size()); + EXPECT_EQ(1, invalidRemoteIpAddressCounter); + EXPECT_EQ(1, receiver.invalidIpAddressPdus()); + EXPECT_EQ(0, receiver.failedMemAllocPdus()); +} + +/** + * \desc: Check that reception of a UDP packet fails if memory allocation does not succeed. + */ +TEST_F(ReceiverTest, receive_udp_packet_with_memory_allocation_failure) +{ + constexpr size_t NEW_MAX_ELEMENT_SIZE = 2U; + constexpr size_t NEW_CAPACITY = 10 * NEW_MAX_ELEMENT_SIZE; + using NewQueue = ::io::MemoryQueue; + using NewWriter = ::io::MemoryQueueWriter; + NewQueue newQueue; + NewWriter newWriter(newQueue); + new (&receiver)::io::udp::Receiver(socketMock, newWriter, errorHandler); + + uint8_t const udpPayload[] = { + 0x00, + 0x00, + 0x00, + 0x02, // PDU transport ID + 0x00, + 0x00, + 0x00, + 0x04, // size + 0xff, + 0xaa, + 0xff, + 0xaa // data + }; + + auto element = reader.peek(); + ASSERT_EQ(0, element.size()); + + simulateUdpPacketReception(udpPayload); + + element = reader.peek(); + EXPECT_EQ(0, element.size()); + EXPECT_EQ(0, receiver.invalidIpAddressPdus()); + EXPECT_EQ(1, receiver.failedMemAllocPdus()); + EXPECT_EQ(1, memAllocationFailureCounter); +} + +/** + * \desc + * If the queue is full twice in a row, the error handling is only called once. + */ +TEST_F(ReceiverTest, multiple_drops_one_error_handling) +{ + uint8_t const udpPayload[] = { + 0x00, + 0x00, + 0x00, + 0x02, // PDU transport ID + 0x00, + 0x00, + 0x00, + 0x04, // size + 0xff, + 0xaa, + 0xff, + 0xaa // data + }; + + constexpr size_t NEW_MAX_ELEMENT_SIZE = sizeof(udpPayload); + constexpr size_t NEW_CAPACITY = sizeof(Queue::size_type) + NEW_MAX_ELEMENT_SIZE; + using NewQueue = ::io::MemoryQueue; + using NewWriter = ::io::MemoryQueueWriter; + using NewReader = ::io::MemoryQueueReader; + NewQueue newQueue; + NewWriter newWriter(newQueue); + NewReader newReader(newQueue); + new (&receiver)::io::udp::Receiver(socketMock, newWriter, errorHandler); + + simulateUdpPacketReception(udpPayload); // works + simulateUdpPacketReception(udpPayload); // fails + + EXPECT_EQ(0, receiver.invalidIpAddressPdus()); + EXPECT_EQ(1, receiver.failedMemAllocPdus()); + EXPECT_EQ(1, memAllocationFailureCounter); + + simulateUdpPacketReception(udpPayload); // fails + + EXPECT_EQ(0, receiver.invalidIpAddressPdus()); + EXPECT_EQ(2, receiver.failedMemAllocPdus()); + EXPECT_EQ(1, memAllocationFailureCounter); // not updated + + newReader.release(); + + simulateUdpPacketReception(udpPayload); // works + simulateUdpPacketReception(udpPayload); // fails + + EXPECT_EQ(0, receiver.invalidIpAddressPdus()); + EXPECT_EQ(3, receiver.failedMemAllocPdus()); + EXPECT_EQ(2, memAllocationFailureCounter); // updated +} +} // anonymous namespace diff --git a/libs/bsw/routing/test/src/io/udp/SenderTest.cpp b/libs/bsw/routing/test/src/io/udp/SenderTest.cpp new file mode 100644 index 00000000000..89e5958fb40 --- /dev/null +++ b/libs/bsw/routing/test/src/io/udp/SenderTest.cpp @@ -0,0 +1,114 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "io/udp/Sender.h" + +#include +#include + +#include + +namespace +{ +struct SenderTest : ::testing::Test +{ + static constexpr size_t MAX_ELEMENT_SIZE = 1416U; + static constexpr size_t CAPACITY = 10 * MAX_ELEMENT_SIZE; + using Queue = ::io::MemoryQueue; + using Reader = ::io::MemoryQueueReader; + using Writer = ::io::MemoryQueueWriter; + + SenderTest() : queue(), reader(queue), writer(queue), socketMock() {} + + void simulatePduReception(uint32_t id, ::etl::span data) + { + size_t const HEADER = 8U; + auto memory = writer.allocate(HEADER + data.size()); + ASSERT_FALSE(memory.empty()); + memory.take<::etl::be_uint32_t>() = id; + memory.take<::etl::be_uint32_t>() = data.size(); + ASSERT_TRUE(::etl::copy(data, memory)); + writer.commit(); + } + + Queue queue; + Reader reader = Reader(queue); + Writer writer = Writer(queue); + ::udp::AbstractDatagramSocketMock socketMock; +}; + +/** + * \desc: Check that sending a UDP packet works. + */ +TEST_F(SenderTest, run) +{ + uint8_t const raw_data[] = {1, 2, 3, 4}; + ::etl::span const data(raw_data); + ::io::udp::Sender sender(reader, socketMock); + + auto element = reader.peek(); + ASSERT_TRUE(element.empty()); + + EXPECT_CALL( + socketMock, send(::testing::Matcher<::etl::span const&>(::testing::_))) + .WillRepeatedly( + ::testing::Return(::udp::AbstractDatagramSocketMock::ErrorCode::UDP_SOCKET_OK)); + + simulatePduReception(1, data); + + element = reader.peek(); + ASSERT_NE(element.size(), 0); + + uint8_t expected[] = {0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, 0x03, 0x04}; + ASSERT_THAT(expected, ::testing::ElementsAreArray(element.data(), element.size())); + + sender.run(1); + + element = reader.peek(); + ASSERT_EQ(element.size(), 0); + EXPECT_EQ(0, sender.socketErrorPdus()); +} + +/** + * \desc + * UDP packet is dropped if there is a socket error + */ +TEST_F(SenderTest, send_udp_packet_with_socket_error) +{ + uint8_t const raw_data[] = {1, 2, 3, 4}; + ::etl::span const data(raw_data); + ::io::udp::Sender sender(reader, socketMock); + + auto element = reader.peek(); + ASSERT_TRUE(element.empty()); + + EXPECT_CALL( + socketMock, send(::testing::Matcher<::etl::span const&>(::testing::_))) + .WillOnce( + ::testing::Return(::udp::AbstractDatagramSocketMock::ErrorCode::UDP_SOCKET_NOT_OK)) + .WillRepeatedly( + ::testing::Return(::udp::AbstractDatagramSocketMock::ErrorCode::UDP_SOCKET_OK)); + + simulatePduReception(1, data); + + element = reader.peek(); + ASSERT_NE(element.size(), 0); + + uint8_t expected[] = {0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, 0x03, 0x04}; + ASSERT_THAT(expected, ::testing::ElementsAreArray(element.data(), element.size())); + + sender.run(1); + + element = reader.peek(); + ASSERT_EQ(element.size(), 0); + EXPECT_EQ(1, sender.socketErrorPdus()); +} + +} // anonymous namespace diff --git a/libs/bsw/routing/test/src/routing/ChannelNamesTest.cpp b/libs/bsw/routing/test/src/routing/ChannelNamesTest.cpp new file mode 100644 index 00000000000..93936ec24fe --- /dev/null +++ b/libs/bsw/routing/test/src/routing/ChannelNamesTest.cpp @@ -0,0 +1,126 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/ChannelNames.h" + +#include "blob/configuration.h" +#include "routing/Header.h" +#include "routing/constants.h" +#include "routing/util.h" + +#include + +#include + +namespace +{ +using namespace ::testing; + +/** + * \desc: Loading the channel names config from a test blob gives the correct values. + */ +TEST(ChannelNamesTest, channel_names_config) +{ + uint32_t const expectedOffsets[] = {0, 5, 13, 18, 33, 48, 63, 78, 93, 108, 123, 138, 160}; + char const* expectedChannelNames[] + = {"CAN0", + "FLEXRAY", + "CAN1", + "PDU_TRANSPORT0", + "PDU_TRANSPORT1", + "PDU_TRANSPORT2", + "PDU_TRANSPORT3", + "PDU_TRANSPORT4", + "PDU_TRANSPORT5", + "PDU_TRANSPORT6", + "PDU_TRANSPORT7", + "UNNAMED_PDU_TRANSPORT"}; + + ::routing::ChannelNames channelNamesConfig; + ASSERT_TRUE(::routing::load( + ::routing::config(::blob::CONFIGURATION_BLOB, ::blob::Config::Type::CHANNEL_NAMES).data, + channelNamesConfig)); + + for (size_t i = 0; i < ::routing::NUM_CHANNELS; ++i) + { + auto const size = channelNamesConfig.offsets[i + 1] - channelNamesConfig.offsets[i]; + auto const channelName + = channelNamesConfig.names.subspan(channelNamesConfig.offsets[i], size); + EXPECT_EQ(expectedOffsets[i], channelNamesConfig.offsets[i]); + EXPECT_THAT(channelName, ElementsAreArray(expectedChannelNames[i], channelName.size())); + } +} + +/** + * \desc: The name function returns the correct channel name. + */ +TEST(ChannelNamesTest, name) +{ + ::routing::ChannelNames config; + ASSERT_TRUE(::routing::load( + ::routing::config(::blob::CONFIGURATION_BLOB, ::blob::Config::Type::CHANNEL_NAMES).data, + config)); + + EXPECT_TRUE(::routing::name(config, ::routing::NUM_CHANNELS).empty()); + + for (size_t i = 0; i < ::routing::NUM_CHANNELS; ++i) + { + auto const channelName = ::routing::name(config, i); + auto const expectedChannelName + = config.names.subspan(config.offsets[i], config.offsets[i + 1] - config.offsets[i]); + EXPECT_THAT(channelName, ElementsAreArray(expectedChannelName)); + } +} + +/** + * \desc: The name function returns an empty span if next offset is not greater than current + * offset. + */ +TEST(ChannelNamesTest, name_decreasing_offset) +{ + constexpr uint32_t NUM_CHANNELS = 3; + + uint8_t const data[] + = {0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, + 0x00, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x0F, 0x43, 0x41, 0x4E, 0x30, + 0x00, 0x43, 0x41, 0x4E, 0x32, 0x00, 0x43, 0x41, 0x4E, 0x31, 0x00, 0xFF, 0xFF, 0xFF}; + + ::routing::ChannelNames config; + ASSERT_TRUE(::routing::load( + ::routing::config(::etl::make_span(data), ::blob::Config::Type::CHANNEL_NAMES).data, + config)); + + EXPECT_TRUE(::routing::name(config, NUM_CHANNELS).empty()); + + // next offset is greater than current offset + auto expectedChannelName + = config.names.subspan(config.offsets[0], config.offsets[1] - config.offsets[0]); + EXPECT_THAT(::routing::name(config, 0), ElementsAreArray(expectedChannelName)); + + // next offset is not greater than current offset + EXPECT_TRUE(::routing::name(config, 1).empty()); +} + +/** + * \desc: Loading config from an empty blob doesn't crash. + */ +TEST(ChannelNamesTest, empty_blob) +{ + ::etl::span emptyData; + + ::routing::ChannelNames config; + ASSERT_FALSE(::routing::load( + ::routing::config(emptyData, ::blob::Config::Type::CHANNEL_NAMES).data, config)); + + EXPECT_TRUE(::routing::name(config, 0).empty()); +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/ClampedCounterTest.cpp b/libs/bsw/routing/test/src/routing/ClampedCounterTest.cpp new file mode 100644 index 00000000000..c7135118eae --- /dev/null +++ b/libs/bsw/routing/test/src/routing/ClampedCounterTest.cpp @@ -0,0 +1,114 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/util.h" + +#include + +#include + +namespace +{ +struct ClampedCounterTest : ::testing::Test +{ + using T = uint8_t; + + static constexpr T MAX = ::etl::numeric_limits::max(); + + ::routing::ClampedCounter c; +}; + +constexpr ClampedCounterTest::T ClampedCounterTest::MAX; + +/** + * \desc: Check that prefix increment works. + */ +TEST_F(ClampedCounterTest, prefix_increment) +{ + ASSERT_EQ(c, 0U); + ASSERT_EQ(++c, 1U); + ASSERT_EQ(++c, 2U); + ASSERT_EQ(++c, 3U); +} + +/** + * \desc: Check that postfix incement works. + */ +TEST_F(ClampedCounterTest, postfix_incement) +{ + ASSERT_EQ(c++, 0U); + ASSERT_EQ(c++, 1U); + ASSERT_EQ(c++, 2U); + ASSERT_EQ(c, 3U); +} + +/** + * \desc: Check that addition assignment works. + */ +TEST_F(ClampedCounterTest, addition_assignment) +{ + ASSERT_EQ(c, 0U); + c += 0U; + ASSERT_EQ(c, 0U); + c += 1U; + ASSERT_EQ(c, 1U); + c += 10U; + ASSERT_EQ(c, 11U); + c += 100U; + ASSERT_EQ(c, 111U); +} + +/** + * \desc: Check that counter does not exceed upper bound with prefix increment. + */ +TEST_F(ClampedCounterTest, prefix_increment_upper_bound) +{ + for (uint8_t i = 0U; i < MAX; ++i) + { + ++c; + } + ASSERT_EQ(++c, MAX); + ASSERT_EQ(++c, MAX); + ASSERT_EQ(++c, MAX); +} + +/** + * \desc: Check that counter does not exceed upper bound with postfix increment. + */ +TEST_F(ClampedCounterTest, postfix_increment_upper_bound) +{ + for (uint8_t i = 0U; i < MAX; ++i) + { + c++; + } + c++; + ASSERT_EQ(c++, MAX); + ASSERT_EQ(c++, MAX); + ASSERT_EQ(c, MAX); +} + +/** + * \desc: Check that counter does not exceed upper bound with addition assignment. + */ +TEST_F(ClampedCounterTest, addition_assignment_upper_bound) +{ + c += 100U; + ASSERT_EQ(c, 100U); + c += 100U; + ASSERT_EQ(c, 200U); + c += 100U; + ASSERT_EQ(c, MAX); + c += 0U; + ASSERT_EQ(c, MAX); + c += 100U; + ASSERT_EQ(c, MAX); +} + +} // anonymous namespace diff --git a/libs/bsw/routing/test/src/routing/HeaderTest.cpp b/libs/bsw/routing/test/src/routing/HeaderTest.cpp new file mode 100644 index 00000000000..03def735174 --- /dev/null +++ b/libs/bsw/routing/test/src/routing/HeaderTest.cpp @@ -0,0 +1,198 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/Header.h" + +#include "blob/configuration.h" +#include "routing/constants.h" +#include "routing/util.h" + +#include +#include + +#include + +namespace +{ +using namespace ::testing; + +/** + * \desc: Loading the header of a routing config from a test blob gives the correct values. + */ +TEST(HeaderTest, routing_config) +{ + auto const config + = ::routing::config(::blob::CONFIGURATION_BLOB, ::blob::Config::Type::ROUTING); + + EXPECT_EQ(::blob::Config::Type::ROUTING, config.type); + auto data = config.data; + ::routing::Header header; + EXPECT_TRUE(::routing::load(data, header)); + EXPECT_EQ(::routing::ChannelType::UNKNOWN, header.channelType); + EXPECT_EQ(::routing::INVALID_CHANNEL_ID, header.channelId); +} + +/** + * \desc: Loading the header of a channel names config from a test blob gives the correct values. + */ +TEST(HeaderTest, channel_names_config) +{ + auto const config + = ::routing::config(::blob::CONFIGURATION_BLOB, ::blob::Config::Type::CHANNEL_NAMES); + + EXPECT_EQ(::blob::Config::Type::CHANNEL_NAMES, config.type); + auto data = config.data; + ::routing::Header header; + EXPECT_TRUE(::routing::load(data, header)); + EXPECT_EQ(::routing::ChannelType::UNKNOWN, header.channelType); + EXPECT_EQ(::routing::INVALID_CHANNEL_ID, header.channelId); +} + +/** + * \desc: Loading the header of channel configs from a test blob gives the correct values. + */ +TEST(HeaderTest, channel_config) +{ + constexpr auto NUM_CHANNELS + = ::routing::NUM_FLEXRAY_CHANNELS + ::routing::NUM_PDU_TRANSPORT_CHANNELS; + uint8_t const channelIds[] = {1, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + + ::routing::ChannelType const expectedChannelTypes[NUM_CHANNELS] = { + ::routing::ChannelType::FR, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + }; + + for (size_t i = 0; i < NUM_CHANNELS; ++i) + { + auto const channelConfig = ::routing::config( + ::blob::CONFIGURATION_BLOB, ::blob::Config::Type::CHANNEL, channelIds[i]); + + EXPECT_EQ(::blob::Config::Type::CHANNEL, channelConfig.type); + auto data = channelConfig.data; + ::routing::Header header; + EXPECT_TRUE(::routing::load(data, header)); + EXPECT_EQ(expectedChannelTypes[i], header.channelType) << " i = " << i; + ; + EXPECT_EQ(channelIds[i], header.channelId); + } +} + +/** + * \desc: Loading the header of RX adapter configs from a test blob gives the correct values. + */ +TEST(HeaderTest, rx_adapter_config) +{ + ::routing::ChannelType const expectedChannelTypes[::routing::NUM_CHANNELS] = { + ::routing::ChannelType::CAN, + ::routing::ChannelType::FR, + ::routing::ChannelType::CAN, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + }; + + ::blob::Blob blob(::blob::CONFIGURATION_BLOB); + + for (size_t i = 0; i < ::routing::NUM_CHANNELS; ++i) + { + auto const channelConfig + = ::routing::config(::blob::CONFIGURATION_BLOB, ::blob::Config::Type::RX_ADAPTER, i); + + EXPECT_EQ(::blob::Config::Type::RX_ADAPTER, channelConfig.type); + auto data = channelConfig.data; + ::routing::Header header; + EXPECT_TRUE(::routing::load(data, header)); + EXPECT_EQ(expectedChannelTypes[i], header.channelType) << " i = " << i; + EXPECT_EQ(i, header.channelId); + } +} + +/** + * \desc: Loading the header of TX adapter configs from a test blob gives the correct values. + */ +TEST(HeaderTest, tx_adapter_config) +{ + ::routing::ChannelType const expectedChannelTypes[::routing::NUM_CHANNELS] = { + ::routing::ChannelType::CAN, + ::routing::ChannelType::FR, + ::routing::ChannelType::CAN, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + ::routing::ChannelType::PDU_TRANSPORT, + }; + + ::blob::Blob blob(::blob::CONFIGURATION_BLOB); + + for (size_t i = 0; i < ::routing::NUM_CHANNELS; ++i) + { + auto const channelConfig + = ::routing::config(::blob::CONFIGURATION_BLOB, ::blob::Config::Type::TX_ADAPTER, i); + + EXPECT_EQ(::blob::Config::Type::TX_ADAPTER, channelConfig.type); + auto data = channelConfig.data; + ::routing::Header header; + EXPECT_TRUE(::routing::load(data, header)); + EXPECT_EQ(i, header.channelId); + EXPECT_EQ(expectedChannelTypes[i], header.channelType) << " i = " << i; + } +} + +/** + * \desc: Loading header from an empty blob doesn't crash. + */ +TEST(HeaderTest, empty_config) +{ + ::etl::span emptyData; + auto const config = ::routing::config(emptyData, ::blob::Config::Type::CHANNEL_NAMES); + + EXPECT_EQ(0, config.data.size()); + + auto data = config.data; + ::routing::Header header; + EXPECT_FALSE(::routing::load(data, header)); +} + +/** + * \desc: Extracting config from a test blob with invalid config type returns an empty span. + */ +TEST(HeaderTest, incorrect_config_type) +{ + constexpr uint32_t INVALID_CONFIG_TYPE = 3; + + uint8_t const data[] = {0x00, 0x00, 0x00, 0x0B, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x8B, 0x31, 0x3D, 0x45}; + + EXPECT_EQ( + 0, + ::routing::config(data, static_cast<::blob::Config::Type>(INVALID_CONFIG_TYPE)) + .data.size()); +} +} // namespace diff --git a/libs/bsw/routing/test/src/routing/IntegrationTest.cpp b/libs/bsw/routing/test/src/routing/IntegrationTest.cpp new file mode 100644 index 00000000000..99b8bfe4509 --- /dev/null +++ b/libs/bsw/routing/test/src/routing/IntegrationTest.cpp @@ -0,0 +1,792 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/Integration.h" + +#include "blob/configuration.h" +#include "routing/ErrorHandler.h" +#include "routing/constants.h" + +#include +#include +#include + +#include + +using namespace ::testing; + +namespace +{ +constexpr uint8_t NUM_PDU_TRANSPORT_CHANNELS = ::routing::NUM_PDU_TRANSPORT_CHANNELS; +constexpr uint8_t NUM_CAN_CHANNELS = ::routing::NUM_CAN_CHANNELS; +constexpr uint8_t NUM_FLEXRAY_CHANNELS = ::routing::NUM_FLEXRAY_CHANNELS; + +template +::etl::array channelIds(::etl::span const adapters) +{ + ::etl::array ids; + for (size_t i = 0; i < SIZE; ++i) + { + ids[i] = adapters[i].channelId; + } + return ids; +} + +class IntegrationTest : public ::testing::Test +{ +public: + static constexpr size_t MAX_ELEMENT_SIZE = 72U; + static constexpr size_t CAPACITY = 10 * MAX_ELEMENT_SIZE; + + using Queue = ::io::MemoryQueue; + using Reader = ::io::MemoryQueueReader; + using Writer = ::io::MemoryQueueWriter; + template< + uint8_t PDU_TRANSPORT = NUM_PDU_TRANSPORT_CHANNELS, + uint8_t CAN = NUM_CAN_CHANNELS, + uint8_t FR = NUM_FLEXRAY_CHANNELS, + size_t MAX_PDU_TRANSPORT = MAX_ELEMENT_SIZE, + size_t MAX_CAN = MAX_ELEMENT_SIZE, + size_t MAX_FR = MAX_ELEMENT_SIZE> + using Integration + = ::routing::Integration; + + IntegrationTest() + { + for (size_t i = 0; i < NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + auto& inputQueue = _pduTransportInputQueues.emplace_back(); + auto& reader = _pduTransportInputReaders.emplace_back(inputQueue); + (void)_pduTransportInputWriters.emplace_back(inputQueue); + _pduTransportReaders[i] = &reader; + + auto& outputQueue = _pduTransportOutputQueues.emplace_back(); + (void)_pduTransportOutputReaders.emplace_back(outputQueue); + auto& writer = _pduTransportOutputWriters.emplace_back(outputQueue); + _pduTransportWriters[i] = &writer; + + _pduTransportChannelIds[i] = NUM_CAN_CHANNELS + NUM_FLEXRAY_CHANNELS + i; + } + + for (size_t i = 0; i < NUM_CAN_CHANNELS; ++i) + { + auto& inputQueue = _canInputQueues.emplace_back(); + auto& reader = _canInputReaders.emplace_back(inputQueue); + (void)_canInputWriters.emplace_back(inputQueue); + _canReaders[i] = &reader; + + auto& outputQueue = _canOutputQueues.emplace_back(); + (void)_canOutputReaders.emplace_back(outputQueue); + auto& writer = _canOutputWriters.emplace_back(outputQueue); + _canWriters[i] = &writer; + } + + for (size_t i = 0; i < NUM_FLEXRAY_CHANNELS; ++i) + { + auto& inputQueue = _frInputQueues.emplace_back(); + auto& reader = _frInputReaders.emplace_back(inputQueue); + (void)_frInputWriters.emplace_back(inputQueue); + _frReaders[i] = &reader; + + auto& outputQueue = _frOutputQueues.emplace_back(); + (void)_frOutputReaders.emplace_back(outputQueue); + auto& writer = _frOutputWriters.emplace_back(outputQueue); + _frWriters[i] = &writer; + } + } + +protected: + ::etl::vector _pduTransportInputQueues; + ::etl::vector _pduTransportOutputQueues; + + ::etl::vector _canInputQueues; + ::etl::vector _canOutputQueues; + + ::etl::vector _frInputQueues; + ::etl::vector _frOutputQueues; + + ::etl::vector _pduTransportInputReaders; + ::etl::vector _pduTransportInputWriters; + ::etl::vector _pduTransportOutputReaders; + ::etl::vector _pduTransportOutputWriters; + + ::etl::vector _canInputReaders; + ::etl::vector _canInputWriters; + ::etl::vector _canOutputReaders; + ::etl::vector _canOutputWriters; + + ::etl::vector _frInputReaders; + ::etl::vector _frInputWriters; + ::etl::vector _frOutputReaders; + ::etl::vector _frOutputWriters; + + ::etl::array<::io::IReader*, NUM_PDU_TRANSPORT_CHANNELS> _pduTransportReaders; + ::etl::array<::io::IWriter*, NUM_PDU_TRANSPORT_CHANNELS> _pduTransportWriters; + + ::etl::array<::io::IReader*, NUM_CAN_CHANNELS> _canReaders; + ::etl::array<::io::IWriter*, NUM_CAN_CHANNELS> _canWriters; + + ::etl::array<::io::IReader*, NUM_FLEXRAY_CHANNELS> _frReaders; + ::etl::array<::io::IWriter*, NUM_FLEXRAY_CHANNELS> _frWriters; + + ::etl::array _pduTransportChannelIds; +}; + +void writePdu(uint32_t const id, size_t const size, ::io::IWriter& writer) +{ + auto pdu = writer.allocate(size); + if (!pdu.empty()) + { + pdu.take<::etl::be_uint32_t>() = id; + auto& length = pdu.take<::etl::be_uint32_t>(); + length = pdu.size(); + writer.commit(); + } +} + +/** + * \desc: Initializing integration creates the correct number of adapters. + */ +TEST_F(IntegrationTest, number_of_adapters) +{ + Integration<> integration; + + EXPECT_EQ(0, integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(0, integration.canChannelAdapters().size()); + EXPECT_EQ(0, integration.flexrayChannelAdapters().size()); + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(_pduTransportReaders), + ::etl::make_span(_pduTransportWriters), + ::routing::canChannelIds, + _canReaders, + _canWriters, + ::routing::frChannelIds, + _frReaders, + _frWriters, + {}); + + EXPECT_EQ(NUM_PDU_TRANSPORT_CHANNELS, integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(NUM_CAN_CHANNELS, integration.canChannelAdapters().size()); + EXPECT_EQ(NUM_FLEXRAY_CHANNELS, integration.flexrayChannelAdapters().size()); + + EXPECT_THAT( + channelIds(integration.canChannelAdapters()), + ElementsAreArray(::routing::canChannelIds)); + EXPECT_THAT( + channelIds(integration.flexrayChannelAdapters()), + ElementsAreArray(::routing::frChannelIds)); + EXPECT_THAT( + channelIds(integration.pduTransportChannelAdapters()), + ElementsAreArray(_pduTransportChannelIds)); +} + +/** + * \desc: Initializing integration with empty blob doesn't crash. + */ +TEST_F(IntegrationTest, empty_blob) +{ + Integration<> integration; + ::etl::span emptyBlob; + + integration.init( + emptyBlob, + ::etl::make_span(_pduTransportReaders), + ::etl::make_span(_pduTransportWriters), + ::routing::canChannelIds, + _canReaders, + _canWriters, + ::routing::frChannelIds, + _frReaders, + _frWriters, + {}); + + EXPECT_EQ(0, integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(0, integration.canChannelAdapters().size()); + EXPECT_EQ(0, integration.flexrayChannelAdapters().size()); + + integration.route(); +} + +/** + * \desc: Initializing integration with a smaller number of PDU transport channels doesn't crash. + */ +TEST_F(IntegrationTest, init_smaller_number_of_pdu_transport_channels) +{ + constexpr auto PDU_TRANSPORT_CHANNELS = NUM_PDU_TRANSPORT_CHANNELS - 1; + + Integration<> integration; + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(_pduTransportReaders).first(PDU_TRANSPORT_CHANNELS), + ::etl::make_span(_pduTransportWriters).first(PDU_TRANSPORT_CHANNELS), + ::routing::canChannelIds, + _canReaders, + _canWriters, + ::routing::frChannelIds, + _frReaders, + _frWriters, + {}); + + EXPECT_EQ(PDU_TRANSPORT_CHANNELS, integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(NUM_CAN_CHANNELS, integration.canChannelAdapters().size()); + EXPECT_EQ(NUM_FLEXRAY_CHANNELS, integration.flexrayChannelAdapters().size()); + + EXPECT_THAT( + channelIds(integration.canChannelAdapters()), + ElementsAreArray(::routing::canChannelIds)); + EXPECT_THAT( + channelIds(integration.flexrayChannelAdapters()), + ElementsAreArray(::routing::frChannelIds)); + EXPECT_THAT( + channelIds(integration.pduTransportChannelAdapters()), + ElementsAreArray(::etl::make_span(_pduTransportChannelIds).first(PDU_TRANSPORT_CHANNELS))); + + integration.route(); +} + +/** + * \desc: Initializing integration with a bigger number of PDU transport channels doesn't crash. + */ +TEST_F(IntegrationTest, init_bigger_number_of_pdu_transport_channels) +{ + constexpr auto PDU_TRANSPORT_CHANNELS = NUM_PDU_TRANSPORT_CHANNELS + 1; + + ::etl::array pduTransportReaders; + ::etl::array pduTransportWriters; + + for (size_t i = 0; i < NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + pduTransportReaders[i] = _pduTransportReaders[i]; + pduTransportWriters[i] = _pduTransportWriters[i]; + } + pduTransportReaders.back() = nullptr; + pduTransportWriters.back() = nullptr; + + Integration<> integration; + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(pduTransportReaders), + ::etl::make_span(pduTransportWriters), + ::routing::canChannelIds, + _canReaders, + _canWriters, + ::routing::frChannelIds, + _frReaders, + _frWriters, + {}); + + EXPECT_EQ(NUM_PDU_TRANSPORT_CHANNELS, integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(NUM_CAN_CHANNELS, integration.canChannelAdapters().size()); + EXPECT_EQ(NUM_FLEXRAY_CHANNELS, integration.flexrayChannelAdapters().size()); + + EXPECT_THAT( + channelIds(integration.canChannelAdapters()), + ElementsAreArray(::routing::canChannelIds)); + EXPECT_THAT( + channelIds(integration.flexrayChannelAdapters()), + ElementsAreArray(::routing::frChannelIds)); + EXPECT_THAT( + channelIds(integration.pduTransportChannelAdapters()), + ElementsAreArray(_pduTransportChannelIds)); + + integration.route(); +} + +/** + * \desc: Initializing integration without PDU transport channels doesn't crash. + */ +TEST_F(IntegrationTest, no_pdu_transport_channels) +{ + constexpr size_t PDU_TRANSPORT_CHANNELS = 0; + constexpr auto CAN_CHANNELS = NUM_CAN_CHANNELS; + constexpr auto FR_CHANNELS = NUM_FLEXRAY_CHANNELS; + + Integration integration; + + integration.init( + ::blob::CONFIGURATION_BLOB, + {}, + {}, + ::routing::canChannelIds, + _canReaders, + _canWriters, + ::routing::frChannelIds, + _frReaders, + _frWriters, + {}); + + EXPECT_EQ(PDU_TRANSPORT_CHANNELS, integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(CAN_CHANNELS, integration.canChannelAdapters().size()); + EXPECT_EQ(FR_CHANNELS, integration.flexrayChannelAdapters().size()); + + EXPECT_THAT( + channelIds(integration.canChannelAdapters()), + ElementsAreArray(::routing::canChannelIds)); + EXPECT_THAT( + channelIds(integration.flexrayChannelAdapters()), + ElementsAreArray(::routing::frChannelIds)); + + writePdu(3, 16, _canInputWriters.front()); + writePdu(18, 16, _pduTransportInputWriters.front()); + writePdu(3435969450, 72, _frInputWriters.front()); + + integration.route(); +} + +/** + * \desc: Initializing integration without CAN channels doesn't crash. + */ +TEST_F(IntegrationTest, no_can_channels) +{ + constexpr auto PDU_TRANSPORT_CHANNELS = NUM_PDU_TRANSPORT_CHANNELS; + constexpr size_t CAN_CHANNELS = 0; + constexpr auto FR_CHANNELS = NUM_FLEXRAY_CHANNELS; + + Integration integration; + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(_pduTransportReaders), + ::etl::make_span(_pduTransportWriters), + {}, + {}, + {}, + ::routing::frChannelIds, + _frReaders, + _frWriters, + {}); + + EXPECT_EQ( + PDU_TRANSPORT_CHANNELS - NUM_CAN_CHANNELS, + integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(CAN_CHANNELS, integration.canChannelAdapters().size()); + EXPECT_EQ(FR_CHANNELS, integration.flexrayChannelAdapters().size()); + + EXPECT_THAT( + channelIds( + integration.pduTransportChannelAdapters()), + ElementsAreArray(::etl::make_span(_pduTransportChannelIds) + .first(PDU_TRANSPORT_CHANNELS - NUM_CAN_CHANNELS))); + EXPECT_THAT( + channelIds(integration.flexrayChannelAdapters()), + ElementsAreArray(::routing::frChannelIds)); + + writePdu(3, 16, _canInputWriters.front()); + writePdu(18, 16, _pduTransportInputWriters.front()); + writePdu(3435969450, 72, _frInputWriters.front()); + + integration.route(); +} + +/** + * \desc: Initializing integration without Flexray channels doesn't crash. + */ +TEST_F(IntegrationTest, no_fr_channels) +{ + constexpr auto PDU_TRANSPORT_CHANNELS = NUM_PDU_TRANSPORT_CHANNELS; + constexpr auto CAN_CHANNELS = NUM_CAN_CHANNELS; + constexpr size_t FR_CHANNELS = 0; + + Integration integration; + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(_pduTransportReaders), + ::etl::make_span(_pduTransportWriters), + ::routing::canChannelIds, + _canReaders, + _canWriters, + {}, + {}, + {}, + {}); + + EXPECT_EQ( + PDU_TRANSPORT_CHANNELS - NUM_FLEXRAY_CHANNELS, + integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(CAN_CHANNELS, integration.canChannelAdapters().size()); + EXPECT_EQ(FR_CHANNELS, integration.flexrayChannelAdapters().size()); + + EXPECT_THAT( + channelIds( + integration.pduTransportChannelAdapters()), + ElementsAreArray(::etl::make_span(_pduTransportChannelIds) + .first(PDU_TRANSPORT_CHANNELS - NUM_FLEXRAY_CHANNELS))); + EXPECT_THAT( + channelIds(integration.canChannelAdapters()), + ElementsAreArray(::routing::canChannelIds)); + + writePdu(3, 16, _canInputWriters.front()); + writePdu(18, 16, _pduTransportInputWriters.front()); + writePdu(3435969450, 72, _frInputWriters.front()); + + integration.route(); +} + +/** + * \desc: Adapters with invalid readers or writers are not created. + */ +TEST_F(IntegrationTest, invalid_readers_or_writers) +{ + auto pduTransportReaders = _pduTransportReaders; + auto pduTransportWriters = _pduTransportWriters; + pduTransportReaders.front() = nullptr; + pduTransportWriters.back() = nullptr; + + auto canReaders = _canReaders; + auto canWriters = _canWriters; + canReaders.front() = nullptr; + canWriters.back() = nullptr; + + auto frReaders = _frReaders; + auto frWriters = _frWriters; + frReaders.front() = nullptr; + + Integration<> integration; + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(pduTransportReaders), + ::etl::make_span(pduTransportWriters), + ::routing::canChannelIds, + canReaders, + canWriters, + ::routing::frChannelIds, + frReaders, + frWriters, + {}); + + EXPECT_EQ(NUM_PDU_TRANSPORT_CHANNELS - 2, integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(NUM_CAN_CHANNELS - 2, integration.canChannelAdapters().size()); + EXPECT_EQ(NUM_FLEXRAY_CHANNELS - 1, integration.flexrayChannelAdapters().size()); + + EXPECT_THAT( + channelIds(integration.pduTransportChannelAdapters()), + ElementsAreArray( + ::etl::make_span(_pduTransportChannelIds).subspan(1, NUM_PDU_TRANSPORT_CHANNELS - 2))); + + new (&integration) Integration<>(); + + frReaders.front() = _frReaders.front(); + frWriters.front() = nullptr; + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(pduTransportReaders), + ::etl::make_span(pduTransportWriters), + ::routing::canChannelIds, + canReaders, + canWriters, + ::routing::frChannelIds, + frReaders, + frWriters, + {}); + + EXPECT_EQ(NUM_PDU_TRANSPORT_CHANNELS - 2, integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(NUM_CAN_CHANNELS - 2, integration.canChannelAdapters().size()); + EXPECT_EQ(NUM_FLEXRAY_CHANNELS - 1, integration.flexrayChannelAdapters().size()); + + EXPECT_THAT( + channelIds(integration.pduTransportChannelAdapters()), + ElementsAreArray( + ::etl::make_span(_pduTransportChannelIds).subspan(1, NUM_PDU_TRANSPORT_CHANNELS - 2))); +} + +/** + * \desc: Reinitializing integration has no effect. + */ +TEST_F(IntegrationTest, reinit) +{ + Integration<> integration; + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(_pduTransportReaders), + ::etl::make_span(_pduTransportWriters), + ::routing::canChannelIds, + _canReaders, + _canWriters, + ::routing::frChannelIds, + _frReaders, + _frWriters, + {}); + + EXPECT_EQ(NUM_PDU_TRANSPORT_CHANNELS, integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(NUM_CAN_CHANNELS, integration.canChannelAdapters().size()); + EXPECT_EQ(NUM_FLEXRAY_CHANNELS, integration.flexrayChannelAdapters().size()); + + EXPECT_THAT( + channelIds(integration.canChannelAdapters()), + ElementsAreArray(::routing::canChannelIds)); + EXPECT_THAT( + channelIds(integration.flexrayChannelAdapters()), + ElementsAreArray(::routing::frChannelIds)); + EXPECT_THAT( + channelIds(integration.pduTransportChannelAdapters()), + ElementsAreArray(_pduTransportChannelIds)); + + _pduTransportReaders.fill(nullptr); + _pduTransportWriters.fill(nullptr); + _canReaders.fill(nullptr); + _canWriters.fill(nullptr); + _frReaders.fill(nullptr); + _frWriters.fill(nullptr); + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(_pduTransportReaders), + ::etl::make_span(_pduTransportWriters), + ::routing::canChannelIds, + _canReaders, + _canWriters, + ::routing::frChannelIds, + _frReaders, + _frWriters, + {}); + + EXPECT_EQ(NUM_PDU_TRANSPORT_CHANNELS, integration.pduTransportChannelAdapters().size()); + EXPECT_EQ(NUM_CAN_CHANNELS, integration.canChannelAdapters().size()); + EXPECT_EQ(NUM_FLEXRAY_CHANNELS, integration.flexrayChannelAdapters().size()); + + EXPECT_THAT( + channelIds(integration.canChannelAdapters()), + ElementsAreArray(::routing::canChannelIds)); + EXPECT_THAT( + channelIds(integration.flexrayChannelAdapters()), + ElementsAreArray(::routing::frChannelIds)); + EXPECT_THAT( + channelIds(integration.pduTransportChannelAdapters()), + ElementsAreArray(_pduTransportChannelIds)); +} + +/** + * \desc: Routing somo PDUs from/to different channels works. + */ +TEST_F(IntegrationTest, route_pdus) +{ + Integration<> integration; + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(_pduTransportReaders), + ::etl::make_span(_pduTransportWriters), + ::routing::canChannelIds, + _canReaders, + _canWriters, + ::routing::frChannelIds, + _frReaders, + _frWriters, + {}); + + auto& canInputWriter = _canInputWriters.front(); + writePdu(3, 16, canInputWriter); + writePdu(3, 16, canInputWriter); + + auto& pduTransportInputWriter = _pduTransportInputWriters.front(); + writePdu(17, 16, pduTransportInputWriter); + writePdu(17, 16, pduTransportInputWriter); + + auto& frInputWriter = _frInputWriters.front(); + writePdu(3435969450, 72, frInputWriter); + writePdu(3435969450, 72, frInputWriter); + + auto& canInputReader = _canInputReaders.front(); + auto& pduTransportInputReader = _pduTransportInputReaders.front(); + auto& frInputReader = _frInputReaders.front(); + auto& pduTransportOutputReader = _pduTransportOutputReaders.front(); + auto& canOutputReader = _canOutputReaders.front(); + auto& frOutputReader = _frOutputReaders.front(); + + EXPECT_EQ(16, canInputReader.peek().size()); + EXPECT_EQ(16, pduTransportInputReader.peek().size()); + EXPECT_EQ(72, frInputReader.peek().size()); + EXPECT_EQ(0, pduTransportOutputReader.peek().size()); + EXPECT_EQ(0, canOutputReader.peek().size()); + EXPECT_EQ(0, frOutputReader.peek().size()); + + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + + EXPECT_EQ(16, canInputReader.peek().size()); + EXPECT_EQ(16, pduTransportInputReader.peek().size()); + EXPECT_EQ(72, frInputReader.peek().size()); + EXPECT_EQ(72, pduTransportOutputReader.peek().size()); + EXPECT_EQ(16, canOutputReader.peek().size()); + EXPECT_EQ(16, frOutputReader.peek().size()); + + pduTransportOutputReader.release(); + canOutputReader.release(); + frOutputReader.release(); + + EXPECT_EQ(0, pduTransportOutputReader.peek().size()); + EXPECT_EQ(0, canOutputReader.peek().size()); + EXPECT_EQ(0, frOutputReader.peek().size()); + + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + + EXPECT_EQ(0, canInputReader.peek().size()); + EXPECT_EQ(0, pduTransportInputReader.peek().size()); + EXPECT_EQ(0, frInputReader.peek().size()); + EXPECT_EQ(72, pduTransportOutputReader.peek().size()); + EXPECT_EQ(16, canOutputReader.peek().size()); + EXPECT_EQ(16, frOutputReader.peek().size()); + + pduTransportOutputReader.release(); + canOutputReader.release(); + frOutputReader.release(); + + EXPECT_EQ(0, pduTransportOutputReader.peek().size()); + EXPECT_EQ(0, canOutputReader.peek().size()); + EXPECT_EQ(0, frOutputReader.peek().size()); +} + +/** + * \desc: Routing some PDUs from/to different channels works with a smaller number of + * channels. + */ +TEST_F(IntegrationTest, route_pdus_smaller_number_of_pdu_transport_channels) +{ + constexpr auto PDU_TRANSPORT_CHANNELS = NUM_PDU_TRANSPORT_CHANNELS - 1; + + Integration<> integration; + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(_pduTransportReaders).first(PDU_TRANSPORT_CHANNELS), + ::etl::make_span(_pduTransportWriters).first(PDU_TRANSPORT_CHANNELS), + ::routing::canChannelIds, + _canReaders, + _canWriters, + ::routing::frChannelIds, + _frReaders, + _frWriters, + {}); + + writePdu(3, 16, _canInputWriters.front()); + writePdu(3, 16, _canInputWriters.front()); + writePdu(17, 16, _pduTransportInputWriters.front()); + writePdu(17, 16, _pduTransportInputWriters.front()); + writePdu(3435969450, 72, _frInputWriters.front()); + writePdu(3435969450, 72, _frInputWriters.front()); + + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + + EXPECT_EQ(72, _pduTransportOutputReaders.front().peek().size()); + EXPECT_EQ(16, _canOutputReaders.front().peek().size()); + EXPECT_EQ(16, _frOutputReaders.front().peek().size()); + + _pduTransportOutputReaders.front().release(); + _canOutputReaders.front().release(); + _frOutputReaders.front().release(); + + EXPECT_EQ(0, _pduTransportOutputReaders.front().peek().size()); + EXPECT_EQ(0, _canOutputReaders.front().peek().size()); + EXPECT_EQ(0, _frOutputReaders.front().peek().size()); + + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + + EXPECT_EQ(72, _pduTransportOutputReaders.front().peek().size()); + EXPECT_EQ(16, _canOutputReaders.front().peek().size()); + EXPECT_EQ(16, _frOutputReaders.front().peek().size()); + + _pduTransportOutputReaders.front().release(); + _canOutputReaders.front().release(); + _frOutputReaders.front().release(); + + EXPECT_EQ(0, _pduTransportOutputReaders.front().peek().size()); + EXPECT_EQ(0, _canOutputReaders.front().peek().size()); + EXPECT_EQ(0, _frOutputReaders.front().peek().size()); +} + +/** + * \desc: Routing some PDUs from/to different channels works with a bigger number of + * channels. + */ +TEST_F(IntegrationTest, route_pdus_bigger_number_of_pdu_transport_channels) +{ + constexpr auto PDU_TRANSPORT_CHANNELS = NUM_PDU_TRANSPORT_CHANNELS + 1; + + ::etl::array pduTransportReaders; + ::etl::array pduTransportWriters; + + for (size_t i = 0; i < NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + pduTransportReaders[i] = _pduTransportReaders[i]; + pduTransportWriters[i] = _pduTransportWriters[i]; + } + pduTransportReaders.back() = nullptr; + pduTransportWriters.back() = nullptr; + + Integration<> integration; + + integration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(pduTransportReaders), + ::etl::make_span(pduTransportWriters), + ::routing::canChannelIds, + _canReaders, + _canWriters, + ::routing::frChannelIds, + _frReaders, + _frWriters, + {}); + + writePdu(3, 16, _canInputWriters.front()); + writePdu(3, 16, _canInputWriters.front()); + writePdu(17, 16, _pduTransportInputWriters.front()); + writePdu(17, 16, _pduTransportInputWriters.front()); + writePdu(3435969450, 72, _frInputWriters.front()); + writePdu(3435969450, 72, _frInputWriters.front()); + + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + + EXPECT_EQ(72, _pduTransportOutputReaders.front().peek().size()); + EXPECT_EQ(16, _canOutputReaders.front().peek().size()); + EXPECT_EQ(16, _frOutputReaders.front().peek().size()); + + _pduTransportOutputReaders.front().release(); + _canOutputReaders.front().release(); + _frOutputReaders.front().release(); + + EXPECT_EQ(0, _pduTransportOutputReaders.front().peek().size()); + EXPECT_EQ(0, _canOutputReaders.front().peek().size()); + EXPECT_EQ(0, _frOutputReaders.front().peek().size()); + + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + EXPECT_TRUE(integration.route()); + + EXPECT_EQ(72, _pduTransportOutputReaders.front().peek().size()); + EXPECT_EQ(16, _canOutputReaders.front().peek().size()); + EXPECT_EQ(16, _frOutputReaders.front().peek().size()); + + _pduTransportOutputReaders.front().release(); + _canOutputReaders.front().release(); + _frOutputReaders.front().release(); + + EXPECT_EQ(0, _pduTransportOutputReaders.front().peek().size()); + EXPECT_EQ(0, _canOutputReaders.front().peek().size()); + EXPECT_EQ(0, _frOutputReaders.front().peek().size()); +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/LegacyRxAdapterTest.cpp b/libs/bsw/routing/test/src/routing/LegacyRxAdapterTest.cpp new file mode 100644 index 00000000000..5699d0b7294 --- /dev/null +++ b/libs/bsw/routing/test/src/routing/LegacyRxAdapterTest.cpp @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/LegacyRxAdapter.h" + +#include "routing/ErrorHandler.h" +#include "routing/definition.h" + +#include +#include + +#include + +namespace +{ +using namespace ::testing; + +template +struct LegacyRxAdapterTest +{ + using Queue = ::io::MemoryQueue; + using Reader = ::io::MemoryQueueReader; + using Writer = ::io::MemoryQueueWriter; + using RxAdapter = ::routing::LegacyRxAdapter; + + LegacyRxAdapterTest(::etl::span<::routing::Definition> defs) + : rxQueue(), rxReader(rxQueue), rxWriter(rxQueue) + { + routing::load(rxAdapterTable, defs, 0, rxAdapterTableMem); + (void)rxAdapter.emplace(rxReader, rxAdapterTable, ::routing::ErrorHandler()); + } + + Queue rxQueue; + Reader rxReader; + Writer rxWriter; + + ::routing::RxAdapterTableMem rxAdapterTableMem; + ::routing::RxAdapterTable rxAdapterTable; + + ::etl::optional rxAdapter; +}; + +/** + * \desc + * The max_size function returns the correct value. + */ +TEST(LegacyRxAdapterTest, max_size) +{ + constexpr size_t CAPACITY = 128; + constexpr size_t MAX_ELEMENT_SIZE = 16; + + LegacyRxAdapterTest rxAdapterTest({}); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + EXPECT_EQ(MAX_ELEMENT_SIZE, rxAdapter.maxSize()); +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/LegacyTxAdapterTest.cpp b/libs/bsw/routing/test/src/routing/LegacyTxAdapterTest.cpp new file mode 100644 index 00000000000..2e84de357ca --- /dev/null +++ b/libs/bsw/routing/test/src/routing/LegacyTxAdapterTest.cpp @@ -0,0 +1,593 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/LegacyTxAdapter.h" + +#include "routing/definition.h" + +#include +#include +#include + +#include + +namespace +{ +using namespace ::testing; + +template +struct LegacyTxAdapterTest +{ + using Queue = ::io::MemoryQueue; + using Reader = ::io::MemoryQueueReader; + using Writer = ::io::MemoryQueueWriter; + + LegacyTxAdapterTest(::etl::span<::routing::Definition> defs) + : txQueue() + , txReader(txQueue) + , txWriter(txQueue) + , unknownMessageIdCounter(0) + , memAllocationFailureCounter(0) + , errorHandler( + ::routing::ErrorHandler::Function::create< + LegacyTxAdapterTest, + &LegacyTxAdapterTest::handleError>(*this), + 0U) + { + ::routing::load(txAdapterTable, defs, 0, txAdapterTableMem); + (void)txAdapter.emplace(txWriter, txAdapterTable, errorHandler); + } + + void handleError(::routing::ErrorHandler::StatusCode const statusCode, uint8_t, uint32_t) + { + switch (statusCode) + { + case ::routing::ErrorHandler::StatusCode::UNKNOWN_MESSAGE_ID: + ++unknownMessageIdCounter; + break; + case ::routing::ErrorHandler::StatusCode::MEM_ALLOCATION_FAILURE: + ++memAllocationFailureCounter; + break; + default: break; + } + } + + Queue txQueue; + Reader txReader; + Writer txWriter; + + ::routing::TxAdapterTableMem txAdapterTableMem; + ::routing::TxAdapterTable txAdapterTable; + + uint32_t unknownMessageIdCounter; + uint32_t memAllocationFailureCounter; + + ::routing::ErrorHandler errorHandler; + + ::etl::optional<::routing::LegacyTxAdapter> txAdapter; +}; + +::etl::span allocatePdu( + uint32_t const id, size_t const size, ::io::IWriter& writer, uint8_t const pattern = 0xAB) +{ + auto pdu = writer.allocate(size); + if (!pdu.empty()) + { + auto data = pdu; + data.take<::etl::be_uint32_t>() = id; + auto& length = data.take<::etl::be_uint32_t>(); + length = data.size(); + ::etl::fill(data.begin(), data.end(), pattern); + } + return pdu; +} + +void writePdu( + uint32_t const id, size_t const size, ::io::IWriter& writer, uint8_t const pattern = 0xAB) +{ + auto pdu = allocatePdu(id, size, writer, pattern); + if (!pdu.empty()) + { + writer.commit(); + } +} + +/** + * \desc + * The max_size function returns the correct value. + */ +TEST(LegacyTxAdapterTest, max_size) +{ + constexpr size_t CAPACITY = 128; + constexpr size_t MAX_ELEMENT_SIZE = 16; + + LegacyTxAdapterTest txAdapterTest({}); + auto& txAdapter = *txAdapterTest.txAdapter; + EXPECT_EQ(MAX_ELEMENT_SIZE, txAdapter.maxSize()); +} + +/** + * \desc + * Sending an outgoing message identical to the PDU works + */ +TEST(LegacyTxAdapterTest, identical_pdu) +{ + constexpr uint8_t MESSAGE_ID = 0x01; + constexpr uint32_t HEADER_LENGTH = 8; + constexpr uint32_t PDU_OFFSET = 0; + constexpr uint32_t MESSAGE_LENGTH = 8; + constexpr uint8_t PATTERN = 0xAB; + + ::routing::Definition defs[] + = {::routing::Definition().out(0, MESSAGE_ID, MESSAGE_LENGTH, PDU_OFFSET)}; + LegacyTxAdapterTest<> txAdapterTest(defs); + auto adapter = *txAdapterTest.txAdapter; + auto reader = txAdapterTest.txReader; + + writePdu(MESSAGE_ID, HEADER_LENGTH + MESSAGE_LENGTH, adapter); + + auto res = reader.peek(); + EXPECT_EQ(HEADER_LENGTH + MESSAGE_LENGTH, res.size()); + EXPECT_EQ(1, adapter.pduCounter()); + EXPECT_EQ(HEADER_LENGTH + MESSAGE_LENGTH, adapter.byteCounter()); + EXPECT_EQ(0, adapter.droppedPduCounter()); + EXPECT_EQ(0, adapter.droppedByteCounter()); + + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(MESSAGE_ID, id); + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(MESSAGE_LENGTH, l); + ::etl::array expectedPayload; + expectedPayload.fill(PATTERN); + EXPECT_THAT(res.first(MESSAGE_LENGTH), ElementsAreArray(expectedPayload)); +} + +/** + * \desc + * Sending an outgoing message with space in front of the PDU works (offset) + */ +TEST(LegacyTxAdapterTest, space_before) +{ + constexpr uint8_t MESSAGE_ID = 0x01; + constexpr uint32_t HEADER_LENGTH = 8; + constexpr uint32_t PDU_OFFSET = 4; + constexpr uint32_t PDU_LENGTH = 4; + constexpr uint32_t MESSAGE_LENGTH = PDU_OFFSET + PDU_LENGTH; + constexpr uint8_t PATTERN = 0xAB; + + ::routing::Definition defs[] + = {::routing::Definition().out(0, MESSAGE_ID, MESSAGE_LENGTH, PDU_OFFSET)}; + LegacyTxAdapterTest<> txAdapterTest(defs); + auto adapter = *txAdapterTest.txAdapter; + auto reader = txAdapterTest.txReader; + + writePdu(MESSAGE_ID, HEADER_LENGTH + PDU_LENGTH, adapter); + + auto res = reader.peek(); + EXPECT_EQ(HEADER_LENGTH + MESSAGE_LENGTH, res.size()); + EXPECT_EQ(1, adapter.pduCounter()); + EXPECT_EQ(HEADER_LENGTH + MESSAGE_LENGTH, adapter.byteCounter()); + EXPECT_EQ(0, adapter.droppedPduCounter()); + EXPECT_EQ(0, adapter.droppedByteCounter()); + + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(MESSAGE_ID, id); + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(MESSAGE_LENGTH, l); + ::etl::array expectedFiller; + expectedFiller.fill(0xFF); + EXPECT_THAT(res.take(PDU_OFFSET), ElementsAreArray(expectedFiller)); + ::etl::array expectedPayload; + expectedPayload.fill(PATTERN); + EXPECT_THAT(res, ElementsAreArray(expectedPayload)); +} + +/** + * \desc + * Sending an outgoing message larger than the PDU works (padding) + */ +TEST(LegacyTxAdapterTest, space_after) +{ + constexpr uint8_t MESSAGE_ID = 0x01; + constexpr uint32_t HEADER_LENGTH = 8; + constexpr uint32_t PDU_OFFSET = 0; + constexpr uint32_t PDU_LENGTH = 8; + constexpr uint32_t MESSAGE_LENGTH = PDU_LENGTH + 8; + constexpr uint8_t PATTERN = 0xAB; + + ::routing::Definition defs[] + = {::routing::Definition().out(0, MESSAGE_ID, MESSAGE_LENGTH, PDU_OFFSET)}; + LegacyTxAdapterTest<> txAdapterTest(defs); + auto adapter = *txAdapterTest.txAdapter; + auto reader = txAdapterTest.txReader; + + writePdu(MESSAGE_ID, HEADER_LENGTH + PDU_LENGTH, adapter); + + auto res = reader.peek(); + EXPECT_EQ(HEADER_LENGTH + MESSAGE_LENGTH, res.size()); + EXPECT_EQ(1, adapter.pduCounter()); + EXPECT_EQ(HEADER_LENGTH + MESSAGE_LENGTH, adapter.byteCounter()); + EXPECT_EQ(0, adapter.droppedPduCounter()); + EXPECT_EQ(0, adapter.droppedByteCounter()); + + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(MESSAGE_ID, id); + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(MESSAGE_LENGTH, l); + ::etl::array expectedPayload; + expectedPayload.fill(PATTERN); + EXPECT_THAT(res.take(PDU_LENGTH), ElementsAreArray(expectedPayload)); + ::etl::array expectedFiller; + expectedFiller.fill(0xFF); + EXPECT_THAT(res, ElementsAreArray(expectedFiller)); +} + +/** + * \desc + * Sending a small PDU with an offset works (padding + offset) + */ +TEST(LegacyTxAdapterTest, space_before_after) +{ + constexpr uint8_t MESSAGE_ID = 0x01; + constexpr uint32_t HEADER_LENGTH = 8; + constexpr uint32_t PDU_OFFSET = 2; + constexpr uint32_t PDU_LENGTH = 4; + constexpr uint32_t MESSAGE_LENGTH = 16; + constexpr uint8_t PATTERN = 0xAB; + + ::routing::Definition defs[] + = {::routing::Definition().out(0, MESSAGE_ID, MESSAGE_LENGTH, PDU_OFFSET)}; + LegacyTxAdapterTest<> txAdapterTest(defs); + auto adapter = *txAdapterTest.txAdapter; + auto reader = txAdapterTest.txReader; + + writePdu(MESSAGE_ID, HEADER_LENGTH + PDU_LENGTH, adapter); + + auto res = reader.peek(); + EXPECT_EQ(HEADER_LENGTH + MESSAGE_LENGTH, res.size()); + EXPECT_EQ(1, adapter.pduCounter()); + EXPECT_EQ(HEADER_LENGTH + MESSAGE_LENGTH, adapter.byteCounter()); + EXPECT_EQ(0, adapter.droppedPduCounter()); + EXPECT_EQ(0, adapter.droppedByteCounter()); + + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(MESSAGE_ID, id); + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(MESSAGE_LENGTH, l); + ::etl::array paddingBefore; + paddingBefore.fill(0xFF); + EXPECT_THAT(res.take(PDU_OFFSET), ElementsAreArray(paddingBefore)); + ::etl::array expectedPayload; + expectedPayload.fill(PATTERN); + EXPECT_THAT(res.take(PDU_LENGTH), ElementsAreArray(expectedPayload)); + ::etl::array paddingAfter; + paddingAfter.fill(0xFF); + EXPECT_THAT(res, ElementsAreArray(paddingAfter)); +} + +/** + * \desc + * Sending a small PDU multiple times in a row works. + * */ +TEST(LegacyTxAdapterTest, send_multiple_times) +{ + constexpr uint8_t MESSAGE_ID = 0x01; + constexpr uint32_t HEADER_LENGTH = 8; + constexpr uint32_t PDU_OFFSET = 2; + constexpr uint32_t PDU_LENGTH = 4; + constexpr uint32_t MESSAGE_LENGTH = 16; + constexpr uint8_t PATTERN = 0xAB; + constexpr size_t ITERATIONS = 20; + + ::routing::Definition defs[] + = {::routing::Definition().out(0, MESSAGE_ID, MESSAGE_LENGTH, PDU_OFFSET)}; + LegacyTxAdapterTest<> txAdapterTest(defs); + auto adapter = *txAdapterTest.txAdapter; + auto reader = txAdapterTest.txReader; + + for (size_t i = 0; i < ITERATIONS; i++) + { + writePdu(MESSAGE_ID, HEADER_LENGTH + PDU_LENGTH, adapter); + + EXPECT_EQ(i + 1, adapter.pduCounter()); + EXPECT_EQ((i + 1) * (HEADER_LENGTH + MESSAGE_LENGTH), adapter.byteCounter()); + EXPECT_EQ(0, adapter.droppedPduCounter()); + EXPECT_EQ(0, adapter.droppedByteCounter()); + } + + for (size_t i = 0; i < ITERATIONS; i++) + { + auto res = reader.peek(); + EXPECT_EQ(HEADER_LENGTH + MESSAGE_LENGTH, res.size()); + + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(MESSAGE_ID, id); + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(MESSAGE_LENGTH, l); + ::etl::array paddingBefore; + paddingBefore.fill(0xFF); + EXPECT_THAT(res.take(PDU_OFFSET), ElementsAreArray(paddingBefore)); + ::etl::array expectedPayload; + expectedPayload.fill(PATTERN); + EXPECT_THAT(res.take(PDU_LENGTH), ElementsAreArray(expectedPayload)); + ::etl::array paddingAfter; + paddingAfter.fill(0xFF); + EXPECT_THAT(res, ElementsAreArray(paddingAfter)); + + reader.release(); + } +} + +/** + * \desc + * PDU is dropped on allocation if its size is greater than max element size of queue. + */ +TEST(LegacyTxAdapterTest, drop_pdu_on_allocation_pdu_is_too_big) +{ + constexpr size_t CAPACITY = 64; + constexpr size_t MAX_ELEMENT_SIZE = 16; + + ::routing::Definition defs[] = {::routing::Definition().out(0, 0x1, 8, 0)}; + LegacyTxAdapterTest txAdapterTest(defs); + auto& txAdapter = *txAdapterTest.txAdapter; + + auto pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE + 1, txAdapter); + txAdapter.commit(); + + EXPECT_EQ(0, pdu.size()); + EXPECT_EQ(0, txAdapter.pduCounter()); + EXPECT_EQ(0, txAdapter.byteCounter()); + EXPECT_EQ(1, txAdapter.droppedPduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE + 1, txAdapter.droppedByteCounter()); + EXPECT_EQ(1, txAdapter.oversizedPdus()); + EXPECT_EQ(0, txAdapter.failedMemAllocPdus()); + EXPECT_EQ(0, txAdapter.unknownMessagePdus()); + EXPECT_EQ(0, txAdapter.failedMemMovePdus()); +} + +/** + * \desc + * PDU is dropped on allocation if queue is full. + */ +TEST(LegacyTxAdapterTest, drop_pdu_on_allocation_queue_is_full) +{ + constexpr size_t MAX_ELEMENT_SIZE = 16; + constexpr size_t CAPACITY = 32; + + ::routing::Definition defs[] = {::routing::Definition().out(0, 0x1, 8, 0)}; + LegacyTxAdapterTest txAdapterTest(defs); + auto& txAdapter = *txAdapterTest.txAdapter; + writePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + auto pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE - 1, txAdapter); + + EXPECT_EQ(0, pdu.size()); + EXPECT_EQ(1, txAdapter.pduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE, txAdapter.byteCounter()); + EXPECT_EQ(1, txAdapter.droppedPduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE - 1, txAdapter.droppedByteCounter()); + EXPECT_EQ(0, txAdapter.oversizedPdus()); + EXPECT_EQ(1, txAdapterTest.memAllocationFailureCounter); + EXPECT_EQ(1, txAdapter.failedMemAllocPdus()); + EXPECT_EQ(0, txAdapter.unknownMessagePdus()); + EXPECT_EQ(0, txAdapter.failedMemMovePdus()); +} + +/** + * \desc + * If the queue is full twice in a row, the error handling is only called once. + */ +TEST(LegacyTxAdapterTest, multiple_drops_one_error_handling) +{ + constexpr size_t MAX_ELEMENT_SIZE = 16; + constexpr size_t CAPACITY = 32; + + ::routing::Definition defs[] = {::routing::Definition().out(0, 0x1, 8, 0)}; + LegacyTxAdapterTest txAdapterTest(defs); + auto& txAdapter = *txAdapterTest.txAdapter; + writePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + + auto pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE - 1, txAdapter); + EXPECT_EQ(0, pdu.size()); + EXPECT_EQ(1, txAdapter.pduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE, txAdapter.byteCounter()); + EXPECT_EQ(1, txAdapter.droppedPduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE - 1, txAdapter.droppedByteCounter()); + EXPECT_EQ(0, txAdapter.oversizedPdus()); + EXPECT_EQ(1, txAdapterTest.memAllocationFailureCounter); + EXPECT_EQ(1, txAdapter.failedMemAllocPdus()); + EXPECT_EQ(0, txAdapter.unknownMessagePdus()); + EXPECT_EQ(0, txAdapter.failedMemMovePdus()); + + pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE - 1, txAdapter); + EXPECT_EQ(0, pdu.size()); + EXPECT_EQ(1, txAdapter.pduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE, txAdapter.byteCounter()); + EXPECT_EQ(2, txAdapter.droppedPduCounter()); + EXPECT_EQ((MAX_ELEMENT_SIZE - 1) * 2, txAdapter.droppedByteCounter()); + EXPECT_EQ(0, txAdapter.oversizedPdus()); + EXPECT_EQ(1, txAdapterTest.memAllocationFailureCounter); // only called once + EXPECT_EQ(2, txAdapter.failedMemAllocPdus()); + EXPECT_EQ(0, txAdapter.unknownMessagePdus()); + EXPECT_EQ(0, txAdapter.failedMemMovePdus()); + + // Succesfully allocating a PDU then failing counts again + auto reader = txAdapterTest.txReader; + reader.peek(); + reader.release(); + writePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + + pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE - 1, txAdapter); + EXPECT_EQ(0, pdu.size()); + EXPECT_EQ(2, txAdapter.pduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE * 2, txAdapter.byteCounter()); + EXPECT_EQ(3, txAdapter.droppedPduCounter()); + EXPECT_EQ((MAX_ELEMENT_SIZE - 1) * 3, txAdapter.droppedByteCounter()); + EXPECT_EQ(0, txAdapter.oversizedPdus()); + EXPECT_EQ(2, txAdapterTest.memAllocationFailureCounter); // this one is counted + EXPECT_EQ(3, txAdapter.failedMemAllocPdus()); + EXPECT_EQ(0, txAdapter.unknownMessagePdus()); + EXPECT_EQ(0, txAdapter.failedMemMovePdus()); +} + +/** + * \desc + * PDU is dropped on commit if message ID is unknown. + */ +TEST(LegacyTxAdapterTest, drop_pdu_on_commit_unknown_message_id) +{ + constexpr size_t CAPACITY = 64; + constexpr size_t MAX_ELEMENT_SIZE = 32; + + ::routing::Definition defs[] = {::routing::Definition().out(0, 0x1, 8, 0)}; + LegacyTxAdapterTest txAdapterTest(defs); + auto& txAdapter = *txAdapterTest.txAdapter; + auto& txReader = txAdapterTest.txReader; + + writePdu(0x10, 16, txAdapter); + + EXPECT_EQ(0, txReader.peek().size()); + EXPECT_EQ(0, txAdapter.pduCounter()); + EXPECT_EQ(0, txAdapter.byteCounter()); + EXPECT_EQ(1, txAdapter.droppedPduCounter()); + EXPECT_EQ(16, txAdapter.droppedByteCounter()); + EXPECT_EQ(1, txAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(0, txAdapter.oversizedPdus()); + EXPECT_EQ(0, txAdapter.failedMemAllocPdus()); + EXPECT_EQ(1, txAdapter.unknownMessagePdus()); + EXPECT_EQ(0, txAdapter.failedMemMovePdus()); +} + +/** + * \desc + * PDU is dropped on commit if outgoing message size is greater than max element size of queue. + */ +TEST(LegacyTxAdapterTest, drop_pdu_on_commit_output_message_is_too_big) +{ + constexpr size_t CAPACITY = 64; + constexpr size_t MAX_ELEMENT_SIZE = 16; + + ::routing::Definition defs[] = {::routing::Definition().out(0, 0x1, 16, 0)}; + LegacyTxAdapterTest txAdapterTest(defs); + auto& txAdapter = *txAdapterTest.txAdapter; + auto& txReader = txAdapterTest.txReader; + + writePdu(0x1, 16, txAdapter); + + EXPECT_EQ(0, txReader.peek().size()); + EXPECT_EQ(0, txAdapter.pduCounter()); + EXPECT_EQ(0, txAdapter.byteCounter()); + EXPECT_EQ(1, txAdapter.droppedPduCounter()); + EXPECT_EQ(24, txAdapter.droppedByteCounter()); + EXPECT_EQ(0, txAdapter.oversizedPdus()); + EXPECT_EQ(1, txAdapterTest.memAllocationFailureCounter); + EXPECT_EQ(1, txAdapter.failedMemAllocPdus()); + EXPECT_EQ(0, txAdapter.unknownMessagePdus()); + EXPECT_EQ(0, txAdapter.failedMemMovePdus()); +} + +/** + * \desc + * PDU is dropped on commit if PDU offset is equal to or greater than message length. + */ +TEST(LegacyTxAdapterTest, drop_pdu_on_commit_invalid_offset) +{ + constexpr size_t CAPACITY = 64; + constexpr size_t MAX_ELEMENT_SIZE = 16; + + ::routing::Definition defs[] + = {::routing::Definition().out(0, 0x1, 4, 8), ::routing::Definition().out(0, 0x2, 8, 8)}; + LegacyTxAdapterTest txAdapterTest(defs); + auto& txAdapter = *txAdapterTest.txAdapter; + auto& txReader = txAdapterTest.txReader; + uint32_t expectedDroppedPduCounter = 0U, expectedDroppedByteCounter = 0U; + + writePdu(0x1, 16, txAdapter); + + expectedDroppedPduCounter += 1; + expectedDroppedByteCounter += 12; + + EXPECT_EQ(0, txReader.peek().size()); + EXPECT_EQ(0, txAdapter.pduCounter()); + EXPECT_EQ(0, txAdapter.byteCounter()); + EXPECT_EQ(expectedDroppedPduCounter, txAdapter.droppedPduCounter()); + EXPECT_EQ(expectedDroppedByteCounter, txAdapter.droppedByteCounter()); + EXPECT_EQ(0, txAdapter.oversizedPdus()); + EXPECT_EQ(0, txAdapter.failedMemAllocPdus()); + EXPECT_EQ(0, txAdapter.unknownMessagePdus()); + EXPECT_EQ(expectedDroppedPduCounter, txAdapter.failedMemMovePdus()); + + writePdu(0x2, 16, txAdapter); + + expectedDroppedPduCounter += 1; + expectedDroppedByteCounter += 16; + + EXPECT_EQ(0, txReader.peek().size()); + EXPECT_EQ(0, txAdapter.pduCounter()); + EXPECT_EQ(0, txAdapter.byteCounter()); + EXPECT_EQ(expectedDroppedPduCounter, txAdapter.droppedPduCounter()); + EXPECT_EQ(expectedDroppedByteCounter, txAdapter.droppedByteCounter()); + EXPECT_EQ(0, txAdapter.oversizedPdus()); + EXPECT_EQ(0, txAdapter.failedMemAllocPdus()); + EXPECT_EQ(0, txAdapter.unknownMessagePdus()); + EXPECT_EQ(expectedDroppedPduCounter, txAdapter.failedMemMovePdus()); + + EXPECT_EQ(0, txReader.peek().size()); + EXPECT_EQ(0, txAdapter.pduCounter()); + EXPECT_EQ(0, txAdapter.byteCounter()); + EXPECT_EQ(expectedDroppedPduCounter, txAdapter.droppedPduCounter()); + EXPECT_EQ(expectedDroppedByteCounter, txAdapter.droppedByteCounter()); + EXPECT_EQ(0, txAdapter.oversizedPdus()); + EXPECT_EQ(0, txAdapter.failedMemAllocPdus()); + EXPECT_EQ(0, txAdapter.unknownMessagePdus()); + EXPECT_EQ(expectedDroppedPduCounter, txAdapter.failedMemMovePdus()); +} + +/** + * \desc + * After allocating some data, calling commit twice should write it to the queue only once. + */ +TEST(LegacyTxAdapterTest, commit_twice) +{ + ::routing::Definition defs[] = {::routing::Definition().out(0, 1, 8, 0)}; + LegacyTxAdapterTest<> txAdapterTest(defs); + auto adapter = *txAdapterTest.txAdapter; + auto reader = txAdapterTest.txReader; + + allocatePdu(1, 16, adapter); + adapter.commit(); + adapter.commit(); + + EXPECT_EQ(16, reader.peek().size()); + EXPECT_EQ(1, adapter.pduCounter()); + EXPECT_EQ(16, adapter.byteCounter()); + + reader.release(); + + EXPECT_EQ(0, reader.peek().size()); +} + +/** + * \desc + * Flush triggers the output writer's flush method. + */ +TEST(LegacyTxAdapterTest, flush) +{ + ::io::IWriterMock writerMock; + ::routing::TxAdapterTable txAdapterTable; + + EXPECT_CALL(writerMock, flush()).Times(1); + + ::routing::LegacyTxAdapter txAdapter(writerMock, txAdapterTable, {}); + + txAdapter.flush(); +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/PduRoutingTableTest.cpp b/libs/bsw/routing/test/src/routing/PduRoutingTableTest.cpp new file mode 100644 index 00000000000..87bc8ef8851 --- /dev/null +++ b/libs/bsw/routing/test/src/routing/PduRoutingTableTest.cpp @@ -0,0 +1,117 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/PduRoutingTable.h" +#include "blob/configuration.h" +#include "routing/pduRouting.h" +#include "routing/util.h" + +#include +#include +#include + +#include + +namespace +{ +using namespace ::testing; + +/** + * \desc + * Loading the routing config from a test blob gives the correct values. + */ +TEST(PduRoutingTableTest, routing_config) +{ + ::etl::array<::etl::be_uint32_t, 26> const expectedOutputMessageIds{ + ::etl::be_uint32_t(0x100U), ::etl::be_uint32_t(0x101U), ::etl::be_uint32_t(0x102U), + ::etl::be_uint32_t(0x103U), ::etl::be_uint32_t(0x300), ::etl::be_uint32_t(0x4000U), + ::etl::be_uint32_t(0x2U), ::etl::be_uint32_t(0x1092U), ::etl::be_uint32_t(0x1092U), + ::etl::be_uint32_t(0x1092U), ::etl::be_uint32_t(0x101U), ::etl::be_uint32_t(0x200U), + ::etl::be_uint32_t(0x1U), ::etl::be_uint32_t(0x1000U), ::etl::be_uint32_t(0x1002U), + ::etl::be_uint32_t(0x1003U), ::etl::be_uint32_t(0x1004U), ::etl::be_uint32_t(0x2U), + ::etl::be_uint32_t(0x5U), ::etl::be_uint32_t(0x16U), ::etl::be_uint32_t(0x14DU), + ::etl::be_uint32_t(0x22BU), ::etl::be_uint32_t(0x6FU), ::etl::be_uint32_t(0x29AU), + ::etl::be_uint32_t(0xDEU), ::etl::be_uint32_t(0x3E7U), + }; + + ::etl::array const expectedDestinations + = {3, 3, 3, 3, 1, 4, 3, 3, 3, 3, 0, 0, 0, 4, 4, 4, 4, 0, 0, 3, 5, 7, 3, 8, 4, 11}; + + ::etl::array<::etl::be_uint32_t, 23> const expectedOffsets + = {::etl::be_uint32_t(0U), + ::etl::be_uint32_t(1U), + ::etl::be_uint32_t(2U), + ::etl::be_uint32_t(3U), + ::etl::be_uint32_t(4U), + ::etl::be_uint32_t(5U), + ::etl::be_uint32_t(6U), + ::etl::be_uint32_t(7U), + ::etl::be_uint32_t(8U), + ::etl::be_uint32_t(9U), + ::etl::be_uint32_t(10U), + ::etl::be_uint32_t(11U), + ::etl::be_uint32_t(12U), + ::etl::be_uint32_t(14U), + ::etl::be_uint32_t(15U), + ::etl::be_uint32_t(16U), + ::etl::be_uint32_t(17U), + ::etl::be_uint32_t(18U), + ::etl::be_uint32_t(20U), + ::etl::be_uint32_t(21U), + ::etl::be_uint32_t(22U), + ::etl::be_uint32_t(24U), + ::etl::be_uint32_t(expectedDestinations.size())}; + + auto const routingConfig + = ::blob::config(::blob::CONFIGURATION_BLOB, ::blob::Config::Type::ROUTING); + EXPECT_EQ(::blob::Config::Type::ROUTING, routingConfig.type); + + ::routing::PduRoutingTable table; + EXPECT_TRUE(::routing::load(routingConfig.data, table)); + + EXPECT_THAT(table.destinationOffsets, ElementsAreArray(expectedOffsets)); + EXPECT_THAT(table.destinations, ElementsAreArray(expectedDestinations)); + EXPECT_THAT(table.outputMessageIds, ElementsAreArray(expectedOutputMessageIds)); +} + +/** + * \desc + * Loading from a corrupted config returns a default-constructed table. + */ +TEST(PduRoutingTableTest, corrupted_routing_config) +{ + uint8_t corruptedRoutingConfigData[] + = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC9, 0xF2, 0xDE, 0x11}; + + ::etl::span routingConfigData = ::etl::make_span(corruptedRoutingConfigData); + routingConfigData.take<::etl::be_uint32_t>() = 3U; // config type + + ::routing::PduRoutingTable table; + + EXPECT_FALSE(::routing::load(corruptedRoutingConfigData, table)); +} + +/** + * \desc + * Loading config from an empty blob doesn't crash. + */ +TEST(PduRoutingTableTest, empty_blob) +{ + ::etl::span emptyBlob; + auto const routingConfig = ::routing::config(emptyBlob, ::blob::Config::Type::ROUTING); + EXPECT_EQ(0, routingConfig.data.size()); + + ::routing::PduRoutingTable table; + EXPECT_FALSE(::routing::load(routingConfig.data, table)); +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/PduTransportBufferedWriterTest.cpp b/libs/bsw/routing/test/src/routing/PduTransportBufferedWriterTest.cpp new file mode 100644 index 00000000000..ea01d4d67f3 --- /dev/null +++ b/libs/bsw/routing/test/src/routing/PduTransportBufferedWriterTest.cpp @@ -0,0 +1,423 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/PduTransportBufferedWriter.h" + +#include "io/MemoryQueue.h" + +#include + +#include + +using namespace ::testing; + +namespace +{ +struct PduTransportBufferedWriterTest : ::testing::Test +{ + static size_t const MAX_ELEMENT_SIZE = 1416U; + static size_t const QUEUE_SIZE = 3 * (MAX_ELEMENT_SIZE + sizeof(uint16_t)); + + using SystemTimer = NiceMock; + using Q = ::io::MemoryQueue; + + PduTransportBufferedWriterTest() + : _q(), _w(_q), _mqw(_q), _mqr(_q), _bmqw(_mqw, 1U), _timestamp(0) + { + ON_CALL(_systemTimerMock, getSystemTimeUs32Bit()) + .WillByDefault([&t = _timestamp] { return t; }); + } + + Q _q; + Q::Writer _w; + ::io::MemoryQueueWriter _mqw; + ::io::MemoryQueueReader _mqr; + + ::routing::PduTransportBufferedWriter _bmqw; + + uint32_t _timestamp; + SystemTimer _systemTimerMock; +}; + +size_t const PduTransportBufferedWriterTest::MAX_ELEMENT_SIZE; +size_t const PduTransportBufferedWriterTest::QUEUE_SIZE; + +/** + * \desc + * The value of max_size() of a PduTransportBufferedWriterTest should be equal to the one of the + * used destination IWriter. + */ +TEST_F(PduTransportBufferedWriterTest, max_size_returns_destinations_max_size) +{ + EXPECT_EQ(_w.maxSize(), _bmqw.maxSize()); +} + +/** + * \desc + * Trying to allocate more than maxSize bytes will return an empty span. + */ +TEST_F(PduTransportBufferedWriterTest, allocate_exceeding_max_size_will_return_empty_span) +{ + auto const b = _bmqw.allocate(_bmqw.maxSize() + 1); + EXPECT_EQ(0, b.size()); +} + +/** + * \desc + * Trying to allocate maxSize bytes will return a span of size maxSize. + */ +TEST_F(PduTransportBufferedWriterTest, allocate_max_size_will_return_max_size_span) +{ + auto const b = _bmqw.allocate(_bmqw.maxSize()); + EXPECT_EQ(_bmqw.maxSize(), b.size()); +} + +/** + * \desc + * When allocating memory on a full writer, it shall return an empty span. + */ +TEST_F(PduTransportBufferedWriterTest, allocate_on_full_writer_will_return_empty_span) +{ + constexpr size_t ELEMENT_SIZE = 10; + // the number of elements we can buffer is equal to three times the number of elements that fit + // entirely in one frame. As such, the parenthesis are required here. + for (size_t i = 0; i < 3 * (_bmqw.maxSize() / ELEMENT_SIZE); ++i) + { + auto const b = _bmqw.allocate(ELEMENT_SIZE); + ASSERT_EQ(ELEMENT_SIZE, b.size()); + _bmqw.commit(); + } + _bmqw.flush(); + + auto const b = _bmqw.allocate(ELEMENT_SIZE); + ASSERT_EQ(0, b.size()); +} + +/** + * \desc + * Calling commit() on a PduTransportBufferedWriter before allocating something shall have no effect + * on the destination IWriter of the PduTransportBufferedWriter. + */ +TEST_F(PduTransportBufferedWriterTest, commit_without_allocation_has_no_effect) +{ + EXPECT_EQ(QUEUE_SIZE, _w.available()); + EXPECT_EQ(0, _bmqw.timeoutSends()); + EXPECT_EQ(0, _bmqw.flushes()); + _bmqw.commit(); + EXPECT_EQ(QUEUE_SIZE, _w.available()); + EXPECT_EQ(0, _bmqw.timeoutSends()); + EXPECT_EQ(0, _bmqw.flushes()); +} + +/** + * \desc + * Writing destination.maxSize triggers a flush of the buffer when allocating the + * next byte. + */ +TEST_F(PduTransportBufferedWriterTest, trigger_buffer_flush) +{ + EXPECT_EQ(0, _bmqw.timeoutSends()); + EXPECT_EQ(0, _bmqw.flushes()); + + for (size_t i = 0; i < _bmqw.maxSize(); i++) + { + auto const b = _bmqw.allocate(1); + ASSERT_EQ(1, b.size()); + _bmqw.commit(); + } + ASSERT_EQ(0, _mqr.peek().size()); + EXPECT_EQ(0, _bmqw.timeoutSends()); + EXPECT_EQ(0, _bmqw.flushes()); + + auto const b = _bmqw.allocate(1); + ASSERT_EQ(1, b.size()); + EXPECT_EQ(_bmqw.maxSize(), _mqr.peek().size()); + EXPECT_EQ(0, _bmqw.timeoutSends()); + EXPECT_EQ(1, _bmqw.flushes()); +} + +/** + * \example + * This examples shows howto transmit 2000 single bytes through a buffered writer. + */ +TEST_F(PduTransportBufferedWriterTest, transmit_2000_bytes) +{ + EXPECT_EQ(0, _bmqw.timeoutSends()); + EXPECT_EQ(0, _bmqw.flushes()); + for (size_t i = 0; i < 2000; ++i) + { + auto b = _bmqw.allocate(1); + ASSERT_EQ(1, b.size()); + b[0] = static_cast(i); + _bmqw.commit(); + } + EXPECT_EQ(0, _bmqw.timeoutSends()); + EXPECT_EQ(1, _bmqw.flushes()); + _bmqw.flush(); + EXPECT_EQ(0, _bmqw.timeoutSends()); + EXPECT_EQ(2, _bmqw.flushes()); + size_t transmitted = 0; + uint8_t expectedValue = 0; + while (!_mqr.peek().empty()) + { + auto const b = _mqr.peek(); + for (auto v : b) + { + EXPECT_EQ(expectedValue, v); + ++expectedValue; + } + transmitted += b.size(); + _mqr.release(); + } + EXPECT_EQ(2000, transmitted); +} + +/** + * \desc + * Flushing the current buffer updates the timestamp. + */ +TEST_F(PduTransportBufferedWriterTest, flush_updates_timestamp) +{ + uint32_t timeouts[] = {1, 2, 3, 5, 10}; + + for (auto const timeout : timeouts) + { + ::routing::PduTransportBufferedWriter w(_mqw, timeout); + _timestamp = 0; + + EXPECT_EQ(0, _mqr.peek().size()); + + w.allocate(1); + w.commit(); + w.flush(); + + EXPECT_EQ(1, _mqr.peek().size()); + _mqr.release(); + + _timestamp = timeout * 1000U; + + w.checkTransmissionTimeout(); + + EXPECT_EQ(0, _mqr.peek().size()); + } +} + +/** + * \desc + * If the transmission timeout is equal to zero, commiting data also flushes the buffer. + */ +TEST_F(PduTransportBufferedWriterTest, commit_triggers_flush_zero_transmission_timeout) +{ + ::routing::PduTransportBufferedWriter w(_mqw, 0U); + + w.checkTransmissionTimeout(); + + EXPECT_EQ(0, _mqr.peek().size()); + + w.allocate(1); + w.commit(); + + EXPECT_EQ(1, w.timeoutSends()); + EXPECT_EQ(1, w.flushes()); + EXPECT_EQ(1, _mqr.peek().size()); + _mqr.release(); + + w.checkTransmissionTimeout(); + + EXPECT_EQ(1, w.timeoutSends()); + EXPECT_EQ(1, w.flushes()); + EXPECT_EQ(0, _mqr.peek().size()); +} + +/** + * \desc + * The expiration of the transmission timeout triggers a flush of the current buffer. + */ +TEST_F(PduTransportBufferedWriterTest, expiration_of_transmission_timeout_triggers_flush) +{ + constexpr uint32_t T = 10; + uint32_t timeouts[] = {1, 2, 3, 5, 10}; + + for (auto const timeout : timeouts) + { + ::routing::PduTransportBufferedWriter w(_mqw, timeout); + + _timestamp = 0; + for (uint32_t i = 0; i < T; ++i, _timestamp = 1000U * i) + { + w.checkTransmissionTimeout(); + + if ((i == 0) || (i % timeout != 0)) + { + EXPECT_EQ(0U, _mqr.peek().size()); + } + else + { + EXPECT_EQ(timeout, _mqr.peek().size()); + _mqr.release(); + } + + w.allocate(1); + w.commit(); + } + uint32_t sends = (T - 1) / timeout; + EXPECT_EQ(sends, w.timeoutSends()); + EXPECT_EQ(sends, w.flushes()); + } +} + +/** + * \desc + * if the transmission timeout will expire in under 1ms, the current buffer is flushed. + */ +TEST_F( + PduTransportBufferedWriterTest, expiration_of_transmission_timeout_in_under_1ms_triggers_flush) +{ + constexpr uint32_t T = 10; + uint32_t timeouts[] = {1, 2, 3, 5, 10}; + + for (auto const timeout : timeouts) + { + ::routing::PduTransportBufferedWriter w(_mqw, timeout); + + for (uint32_t i = 0; i < T; ++i) + { + if ((i == 0) || (i % timeout != 0)) + { + _timestamp = 1000U * i; + w.checkTransmissionTimeout(); + EXPECT_EQ(0U, _mqr.peek().size()); + } + else + { + _timestamp += 1U; + w.checkTransmissionTimeout(); + EXPECT_EQ(timeout, _mqr.peek().size()); + _mqr.release(); + _timestamp = 1000U * i; + } + + w.allocate(1); + w.commit(); + } + uint32_t sends = (T - 1) / timeout; + EXPECT_EQ(sends, w.timeoutSends()); + EXPECT_EQ(sends, w.flushes()); + } +} + +/** + * \desc + * Reset discards previously allocated data. + */ +TEST_F(PduTransportBufferedWriterTest, reset_discards_allocated_data) +{ + uint32_t timeouts[] = {0, 1, 2, 3, 5, 10}; + for (auto const timeout : timeouts) + { + ::routing::PduTransportBufferedWriter w(_mqw, timeout); + _timestamp = 0; + + w.allocate(1); + + w.reset(); + + w.commit(); + w.flush(); + + EXPECT_EQ(0, w.timeoutSends()); + EXPECT_EQ(0, w.flushes()); + EXPECT_EQ(0, _mqr.peek().size()); + } +} + +/** + * \desc + * Reset updates the timestamp and discards previously committed data. + */ +TEST_F(PduTransportBufferedWriterTest, reset_updates_timestamp_and_discards_committed_data) +{ + uint32_t timeouts[] = {1, 2, 3, 5, 10}; + for (auto const timeout : timeouts) + { + ::routing::PduTransportBufferedWriter w(_mqw, timeout); + _timestamp = 0; + + w.allocate(1); + w.commit(); + + w.reset(); + + _timestamp = timeout * 1000U; + + w.checkTransmissionTimeout(); + + EXPECT_EQ(0, _mqr.peek().size()); + + w.allocate(1); + w.commit(); + + w.reset(); + + w.flush(); + + EXPECT_EQ(0, _mqr.peek().size()); + } +} + +/** + * \desc + * Flush still commits data if the timestamp is equal to its maximum value. + */ +TEST_F(PduTransportBufferedWriterTest, flush_max_timestamp) +{ + uint32_t timeouts[] = {1, 2, 3, 5, 10}; + for (auto const timeout : timeouts) + { + ::routing::PduTransportBufferedWriter w(_mqw, timeout); + _timestamp = std::numeric_limits::max(); + + w.allocate(1); + w.commit(); + + _timestamp = timeout * 1000U; + + w.flush(); + + EXPECT_EQ(1, _mqr.peek().size()); + _mqr.release(); + } +} + +/** + * \desc + * Transmission timeout still triggers a flush if the timestamp is equal to its maximum value. + */ +TEST_F(PduTransportBufferedWriterTest, transmission_timeout_max_timestamp) +{ + uint32_t timeouts[] = {1, 2, 3, 5, 10}; + for (auto const timeout : timeouts) + { + ::routing::PduTransportBufferedWriter w(_mqw, timeout); + _timestamp = std::numeric_limits::max(); + + w.allocate(1); + w.commit(); + + _timestamp = timeout * 1000U; + + w.checkTransmissionTimeout(); + + EXPECT_EQ(1, _mqr.peek().size()); + _mqr.release(); + } +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/PduTransportConfigTest.cpp b/libs/bsw/routing/test/src/routing/PduTransportConfigTest.cpp new file mode 100644 index 00000000000..79d32fc9b8e --- /dev/null +++ b/libs/bsw/routing/test/src/routing/PduTransportConfigTest.cpp @@ -0,0 +1,185 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/PduTransportConfig.h" + +#include "blob/configuration.h" +#include "routing/constants.h" +#include "routing/util.h" + +#include + +#include + +namespace +{ +using namespace ::testing; + +/** + * \desc: Loading the PDU transport config from a test blob gives the correct values. + */ +TEST(PduTransportConfigTest, pdu_transport_config) +{ + constexpr uint8_t FIRST_PDU_TRANSPORT_CHANNEL_ID + = ::routing::NUM_CAN_CHANNELS + ::routing::NUM_FLEXRAY_CHANNELS; + routing::PduTransportConfig::Mode const modes[::routing::NUM_PDU_TRANSPORT_CHANNELS] + = {routing::PduTransportConfig::Mode::RX_TX, + routing::PduTransportConfig::Mode::RX_TX, + routing::PduTransportConfig::Mode::TX, + routing::PduTransportConfig::Mode::RX, + routing::PduTransportConfig::Mode::TX, + routing::PduTransportConfig::Mode::TX, + routing::PduTransportConfig::Mode::RX, + routing::PduTransportConfig::Mode::RX, + routing::PduTransportConfig::Mode::TX}; + + uint8_t const pcps[::routing::NUM_PDU_TRANSPORT_CHANNELS] = {0, 1, 0, 0, 2, 0, 3, 0, 7}; + uint16_t const trasmissionTimeouts[::routing::NUM_PDU_TRANSPORT_CHANNELS] + = {0, 0, 1, 0, 2, 3, 0, 0, 5}; + + std::vector> remoteIpAddresses + = {{0, 0, 0, 0}, + {0, 0, 0, 0, 1, 1, 1, 1}, + {0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2}, + {}, + {}, + {}, + {}, + {}, + {}}; + + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + routing::PduTransportConfig pduTransportConfig; + auto const channelConfig = ::routing::config( + ::blob::CONFIGURATION_BLOB, + ::blob::Config::Type::CHANNEL, + FIRST_PDU_TRANSPORT_CHANNEL_ID + i); + EXPECT_TRUE(routing::load(channelConfig.data, pduTransportConfig)); + + EXPECT_EQ(ip::make_ip4(239, 192, 255, 253), pduTransportConfig.ipAddress); + EXPECT_EQ(modes[i], pduTransportConfig.mode); + EXPECT_EQ(1, pduTransportConfig.type); + EXPECT_EQ(4446, pduTransportConfig.localPort); + EXPECT_EQ(4446, pduTransportConfig.remotePort); + EXPECT_EQ(pcps[i], pduTransportConfig.pcp); + EXPECT_EQ(trasmissionTimeouts[i], pduTransportConfig.transmissionTimeout); + EXPECT_THAT(pduTransportConfig.remoteIpAddresses, ElementsAreArray(remoteIpAddresses[i])); + } +} + +/** + * \desc: Loading PDU transport config with address family IPv6 gives the correct values. + */ +TEST(PduTransportConfigTest, ipv6_pdu_transport_config) +{ + constexpr ip::IPAddress IP_ADDRESS = ip::make_ip6(0x0, 0x0, 0x0, 0xEFC0FFFD); + constexpr routing::PduTransportConfig::Mode MODE = routing::PduTransportConfig::Mode::RX_TX; + constexpr uint8_t CONNECTION_TYPE = 1U; + constexpr uint16_t LOCAL_PORT = 4445U; + constexpr uint16_t REMOTE_PORT = 4446U; + constexpr uint8_t PCP = 0U; + constexpr uint16_t TRANSMISSION_TIMEOUT = 0U; + constexpr etl::array REMOTE_IP_ADDRESSES + = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + + uint8_t channelConfigData[] + = {0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x48, 0x01, 0x11, 0x00, 0x00, 0x11, 0x5D, 0x11, 0x5E, 0x00, 0x06, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEF, 0xC0, 0xFF, 0xFD, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + + ::routing::PduTransportConfig pduTransportConfig; + + EXPECT_TRUE(routing::load(channelConfigData, pduTransportConfig)); + + EXPECT_EQ(IP_ADDRESS, pduTransportConfig.ipAddress); + EXPECT_EQ(MODE, pduTransportConfig.mode); + EXPECT_EQ(CONNECTION_TYPE, pduTransportConfig.type); + EXPECT_EQ(LOCAL_PORT, pduTransportConfig.localPort); + EXPECT_EQ(REMOTE_PORT, pduTransportConfig.remotePort); + EXPECT_EQ(PCP, pduTransportConfig.pcp); + EXPECT_EQ(TRANSMISSION_TIMEOUT, pduTransportConfig.transmissionTimeout); + EXPECT_THAT(pduTransportConfig.remoteIpAddresses, ElementsAreArray(REMOTE_IP_ADDRESSES)); +} + +/** + * \desc: Loading from a corrupted config returns a default-constructed config. + */ +TEST(PduTransportConfigTest, corrupted_pdu_transport_config) +{ + uint8_t data[] = {0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x01, 0x11, 0x00, 0x00, 0x11, 0x5D, 0x11, 0x5E, + 0x00, 0x04, 0xEF, 0xC0, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0xFF, 0xFF, 0x69, 0xBF, 0x16, 0xEA}; + + uint8_t corruptedPduTransportConfigData[sizeof(data)]; + ::etl::copy(::etl::make_span(data), ::etl::make_span(corruptedPduTransportConfigData)); + + { + ::etl::span pduTransportConfigData + = ::etl::make_span(corruptedPduTransportConfigData); + pduTransportConfigData.take<::etl::be_uint32_t>() = 3U; // config type + } + + ::routing::PduTransportConfig table; + + EXPECT_FALSE(routing::load(corruptedPduTransportConfigData, table)); + + ::etl::copy(::etl::make_span(data), ::etl::make_span(corruptedPduTransportConfigData)); + + { + ::etl::span pduTransportConfigData + = ::etl::make_span(corruptedPduTransportConfigData); + (void)pduTransportConfigData.take<::etl::be_uint32_t>(); // config type + pduTransportConfigData.take<::etl::be_uint16_t>() = 3U; // channel type + } + + EXPECT_FALSE(routing::load(corruptedPduTransportConfigData, table)); + + ::etl::copy(::etl::make_span(data), ::etl::make_span(corruptedPduTransportConfigData)); + + { + ::etl::span pduTransportConfigData + = ::etl::make_span(corruptedPduTransportConfigData); + (void)pduTransportConfigData.take<::etl::be_uint32_t>(); // config type + (void)pduTransportConfigData.take<::etl::be_uint16_t>(); // channel type + (void)pduTransportConfigData.take(); // unused + (void)pduTransportConfigData.take(); // channel ID + (void)pduTransportConfigData.take<::etl::be_uint32_t>(); // reserved + (void)pduTransportConfigData.take<::etl::be_uint32_t>(); // size + (void)pduTransportConfigData.take(); // connection type + (void)pduTransportConfigData.take(); // mode + (void)pduTransportConfigData.take<::etl::be_uint16_t>(); // vlan ID + (void)pduTransportConfigData.take<::etl::be_uint16_t>(); // local port + (void)pduTransportConfigData.take<::etl::be_uint16_t>(); // remote port + pduTransportConfigData.take<::etl::be_uint16_t>() = 3U; // IP version + } + + EXPECT_FALSE(routing::load(corruptedPduTransportConfigData, table)); +} + +/** + * \desc: Loading config from an empty blob doesn't crash. + */ +TEST(PduTransportConfigTest, empty_blob) +{ + ::etl::span emptyBlob; + + routing::PduTransportConfig pduTransportConfig; + auto const channelConfig = ::routing::config(emptyBlob, ::blob::Config::Type::CHANNEL, 42); + + EXPECT_EQ(0, channelConfig.data.size()); + EXPECT_FALSE(routing::load(channelConfig.data, pduTransportConfig)); +} +} // namespace diff --git a/libs/bsw/routing/test/src/routing/PduTransportIntegrationTest.cpp b/libs/bsw/routing/test/src/routing/PduTransportIntegrationTest.cpp new file mode 100644 index 00000000000..6169b4c80c0 --- /dev/null +++ b/libs/bsw/routing/test/src/routing/PduTransportIntegrationTest.cpp @@ -0,0 +1,751 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/PduTransportIntegration.h" + +#include "blob/configuration.h" +#include "routing/ErrorHandler.h" +#include "routing/PduTransportConfig.h" +#include "routing/constants.h" +#include "routing/util.h" + +#include +#include +#include + +#include + +using namespace ::testing; + +namespace +{ +class PduTransportIntegrationTest : public ::testing::Test +{ +public: + static constexpr size_t MAX_ELEMENT_SIZE = 1416U; + static constexpr uint8_t FIRST_PDU_TRANSPORT_CHANNEL_ID + = ::routing::NUM_CAN_CHANNELS + ::routing::NUM_FLEXRAY_CHANNELS; + + using PduTransportIntegration = ::routing:: + PduTransportIntegration<::routing::NUM_PDU_TRANSPORT_CHANNELS, MAX_ELEMENT_SIZE>; + + PduTransportIntegrationTest() : _pduTransportIntegration(), _timestamp(0) + { + ON_CALL(_systemTimerMock, getSystemTimeUs32Bit()) + .WillByDefault([&t = _timestamp] { return t; }); + + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + auto& inputSocket = _lwipInputSockets.emplace_back(); + ON_CALL(inputSocket, bind(_, _)) + .WillByDefault(Return(::udp::AbstractDatagramSocket::ErrorCode::UDP_SOCKET_OK)); + ON_CALL(inputSocket, join(_)) + .WillByDefault(Return(::udp::AbstractDatagramSocket::ErrorCode::UDP_SOCKET_OK)); + _inputSockets[i] = &inputSocket; + + auto& outputSocket = _lwipOutputSockets.emplace_back(); + ON_CALL(outputSocket, bind(_, _)) + .WillByDefault(Return(::udp::AbstractDatagramSocket::ErrorCode::UDP_SOCKET_OK)); + ON_CALL(outputSocket, connect(_, _, _)) + .WillByDefault(Return(::udp::AbstractDatagramSocket::ErrorCode::UDP_SOCKET_OK)); + ON_CALL(outputSocket, send(Matcher<::etl::span const&>(_))) + .WillByDefault(Return(::udp::AbstractDatagramSocket::ErrorCode::UDP_SOCKET_OK)); + _outputSockets[i] = &outputSocket; + } + + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + _pduTransportChannelIds[i] = FIRST_PDU_TRANSPORT_CHANNEL_ID + i; + auto const channelConfig = ::routing::config( + ::blob::CONFIGURATION_BLOB, + ::blob::Config::Type::CHANNEL, + _pduTransportChannelIds[i]); + ::routing::PduTransportConfig pduTransportConfig; + EXPECT_TRUE(::routing::load(channelConfig.data, pduTransportConfig)); + + _modes[i] = pduTransportConfig.mode; + _transmissionTimeouts[i] = pduTransportConfig.transmissionTimeout; + } + + _pduTransportIntegration.init( + ::blob::CONFIGURATION_BLOB, _pduTransportChannelIds, _inputSockets, _outputSockets); + } + +protected: + using Socket = NiceMock<::udp::AbstractDatagramSocketMock>; + using SystemTimer = NiceMock; + + void commitToAll(size_t const n) + { + for (auto& writer : _pduTransportIntegration.bufferedOutputWriters()) + { + (void)writer.allocate(n); + writer.commit(); + } + } + + void flushAll() + { + for (auto& writer : _pduTransportIntegration.bufferedOutputWriters()) + { + writer.flush(); + } + } + + void writeToAll(size_t const n) + { + commitToAll(n); + flushAll(); + } + + bool isRx(size_t const i) + { + return (_modes[i] == ::routing::PduTransportConfig::Mode::RX_TX) + || (_modes[i] == ::routing::PduTransportConfig::Mode::RX); + } + + bool isTx(size_t const i) + { + return (_modes[i] == ::routing::PduTransportConfig::Mode::RX_TX) + || (_modes[i] == ::routing::PduTransportConfig::Mode::TX); + } + + ::etl::vector _lwipInputSockets; + ::etl::vector _lwipOutputSockets; + + ::etl::array<::udp::AbstractDatagramSocket*, ::routing::NUM_PDU_TRANSPORT_CHANNELS> + _inputSockets; + ::etl::array<::udp::AbstractDatagramSocket*, ::routing::NUM_PDU_TRANSPORT_CHANNELS> + _outputSockets; + + ::etl::array _pduTransportChannelIds; + ::etl::array<::routing::PduTransportConfig::Mode, ::routing::NUM_PDU_TRANSPORT_CHANNELS> _modes; + ::etl::array _transmissionTimeouts; + + ::routing::PduTransportIntegration<::routing::NUM_PDU_TRANSPORT_CHANNELS, MAX_ELEMENT_SIZE> + _pduTransportIntegration; + + ::ip::IPAddress const _ipAddress = ::ip::make_ip4(0, 0, 0, 0); + uint16_t const _vlanId = 0U; + + SystemTimer _systemTimerMock; + uint32_t _timestamp; +}; + +/** + * \desc: Queues, readers and writers are created without initialization. + */ +TEST_F(PduTransportIntegrationTest, members_created_without_init) +{ + new (&_pduTransportIntegration) PduTransportIntegration(); + + EXPECT_EQ(::routing::NUM_PDU_TRANSPORT_CHANNELS, _pduTransportIntegration.inputQueues().size()); + EXPECT_EQ( + ::routing::NUM_PDU_TRANSPORT_CHANNELS, _pduTransportIntegration.outputQueues().size()); + EXPECT_EQ( + ::routing::NUM_PDU_TRANSPORT_CHANNELS, _pduTransportIntegration.inputReaders().size()); + EXPECT_EQ( + ::routing::NUM_PDU_TRANSPORT_CHANNELS, _pduTransportIntegration.inputWriters().size()); + EXPECT_EQ( + ::routing::NUM_PDU_TRANSPORT_CHANNELS, _pduTransportIntegration.outputWriters().size()); + EXPECT_EQ(0, _pduTransportIntegration.bufferedOutputWriters().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpSenders().size()); +} + +/** + * \desc: Reinitialization has no effect. + */ +TEST_F(PduTransportIntegrationTest, reinit) +{ + _pduTransportIntegration.init({}, {}, {}, {}); + + EXPECT_EQ( + ::routing::NUM_PDU_TRANSPORT_CHANNELS, + _pduTransportIntegration.bufferedOutputWriters().size()); +} + +/** + * \desc: Activating and disabling channels change input and output sockets. + */ +TEST_F(PduTransportIntegrationTest, activate_and_disable) +{ + constexpr size_t N = 3, RX = 5, TX = 6; + + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + auto& inputSocket = _lwipInputSockets[i]; + if (isRx(i)) + { + EXPECT_CALL(inputSocket, bind(_, _)).Times(N); + EXPECT_CALL(inputSocket, join(_)).Times(N); + } + EXPECT_CALL(inputSocket, close()).Times(N); + + auto& outputSocket = _lwipOutputSockets[i]; + if (isTx(i)) + { + EXPECT_CALL(outputSocket, bind(_, _)).Times(N); + EXPECT_CALL(outputSocket, connect(_, _, _)).Times(N); + } + EXPECT_CALL(outputSocket, close()).Times(N); + EXPECT_CALL(outputSocket, disconnect()).Times(N); + } + + for (size_t i = 0; i < N; ++i) + { + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + EXPECT_EQ(RX, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(TX, _pduTransportIntegration.udpSenders().size()); + + _pduTransportIntegration.disable(); + EXPECT_EQ(0, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpSenders().size()); + } +} + +/** + * \desc: Activating and disabling channels change input and output sockets with a smaller number of + * channels. + */ +TEST_F(PduTransportIntegrationTest, activate_and_disable_smaller_number_of_pdu_transport_channels) +{ + constexpr uint8_t NUM_PDU_TRANSPORT_CHANNELS = ::routing::NUM_PDU_TRANSPORT_CHANNELS - 1; + constexpr size_t N = 3, RX = 5, TX = 5; + + new (&_pduTransportIntegration) PduTransportIntegration(); + + _pduTransportIntegration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(_pduTransportChannelIds).first(NUM_PDU_TRANSPORT_CHANNELS), + ::etl::make_span(_inputSockets).first(NUM_PDU_TRANSPORT_CHANNELS), + ::etl::make_span(_outputSockets).first(NUM_PDU_TRANSPORT_CHANNELS)); + + for (size_t i = 0; i < NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + auto& inputSocket = _lwipInputSockets[i]; + if (isRx(i)) + { + EXPECT_CALL(inputSocket, bind(_, _)).Times(N); + EXPECT_CALL(inputSocket, join(_)).Times(N); + } + EXPECT_CALL(inputSocket, close()).Times(N); + + auto& outputSocket = _lwipOutputSockets[i]; + if (isTx(i)) + { + EXPECT_CALL(outputSocket, bind(_, _)).Times(N); + EXPECT_CALL(outputSocket, connect(_, _, _)).Times(N); + } + EXPECT_CALL(outputSocket, close()).Times(N); + EXPECT_CALL(outputSocket, disconnect()).Times(N); + } + + for (size_t i = 0; i < N; ++i) + { + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + EXPECT_EQ(RX, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(TX, _pduTransportIntegration.udpSenders().size()); + + _pduTransportIntegration.disable(); + EXPECT_EQ(0, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpSenders().size()); + } +} + +/** + * \desc: Activating and disabling channels change input and output sockets with a bigger number of + * channels. + */ +TEST_F(PduTransportIntegrationTest, activate_and_disable_bigger_number_of_pdu_transport_channels) +{ + constexpr uint8_t NUM_PDU_TRANSPORT_CHANNELS = ::routing::NUM_PDU_TRANSPORT_CHANNELS + 1; + constexpr size_t N = 3, RX = 5, TX = 6; + + ::etl::array pduTransportChannelIds; + ::etl::array<::udp::AbstractDatagramSocket*, NUM_PDU_TRANSPORT_CHANNELS> inputSockets; + ::etl::array<::udp::AbstractDatagramSocket*, NUM_PDU_TRANSPORT_CHANNELS> outputSockets; + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + pduTransportChannelIds[i] = _pduTransportChannelIds[i]; + inputSockets[i] = _inputSockets[i]; + outputSockets[i] = _outputSockets[i]; + } + pduTransportChannelIds.back() = _pduTransportChannelIds.back() + 1; + inputSockets.back() = nullptr; + outputSockets.back() = nullptr; + + new (&_pduTransportIntegration) PduTransportIntegration(); + + _pduTransportIntegration.init( + ::blob::CONFIGURATION_BLOB, pduTransportChannelIds, inputSockets, outputSockets); + + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + auto& inputSocket = _lwipInputSockets[i]; + if (isRx(i)) + { + EXPECT_CALL(inputSocket, bind(_, _)).Times(N); + EXPECT_CALL(inputSocket, join(_)).Times(N); + } + EXPECT_CALL(inputSocket, close()).Times(N); + + auto& outputSocket = _lwipOutputSockets[i]; + if (isTx(i)) + { + EXPECT_CALL(outputSocket, bind(_, _)).Times(N); + EXPECT_CALL(outputSocket, connect(_, _, _)).Times(N); + } + EXPECT_CALL(outputSocket, close()).Times(N); + EXPECT_CALL(outputSocket, disconnect()).Times(N); + } + + for (size_t i = 0; i < N; ++i) + { + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + EXPECT_EQ(RX, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(TX, _pduTransportIntegration.udpSenders().size()); + + _pduTransportIntegration.disable(); + EXPECT_EQ(0, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpSenders().size()); + } +} + +/** + * \desc: No channels are activated if the VLAN ID is invalid. + */ +TEST_F(PduTransportIntegrationTest, activate_invalid_vlan_id) +{ + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + auto& inputSocket = _lwipInputSockets[i]; + EXPECT_CALL(inputSocket, bind(_, _)).Times(0); + EXPECT_CALL(inputSocket, join(_)).Times(0); + + auto& outputSocket = _lwipOutputSockets[i]; + EXPECT_CALL(outputSocket, bind(_, _)).Times(0); + EXPECT_CALL(outputSocket, connect(_, _, _)).Times(0); + } + + _pduTransportIntegration.activate(_ipAddress, _vlanId + 1, {}); + EXPECT_EQ(0, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpSenders().size()); +} + +/** + * \desc: Sending frames to active and disabled channels works. + */ +TEST_F(PduTransportIntegrationTest, send_frames_to_active_and_disabled_channels) +{ + constexpr size_t N = 16; + + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + if (isTx(i)) + { + EXPECT_CALL(_lwipOutputSockets[i], send(Matcher<::etl::span const&>(_))) + .Times(3); + } + else + { + EXPECT_CALL(_lwipOutputSockets[i], send(Matcher<::etl::span const&>(_))) + .Times(0); + } + } + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.disable(); + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); +} + +/** + * \desc: Sending frames to active and disabled channels works with a smaller number of channels. + */ +TEST_F( + PduTransportIntegrationTest, + send_frames_to_active_and_disabled_channels_smaller_number_of_channels) +{ + constexpr uint8_t NUM_PDU_TRANSPORT_CHANNELS = ::routing::NUM_PDU_TRANSPORT_CHANNELS - 1; + constexpr size_t N = 16; + + new (&_pduTransportIntegration) PduTransportIntegration(); + + _pduTransportIntegration.init( + ::blob::CONFIGURATION_BLOB, + ::etl::make_span(_pduTransportChannelIds).first(NUM_PDU_TRANSPORT_CHANNELS), + ::etl::make_span(_inputSockets).first(NUM_PDU_TRANSPORT_CHANNELS), + ::etl::make_span(_outputSockets).first(NUM_PDU_TRANSPORT_CHANNELS)); + + for (size_t i = 0; i < NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + if (isTx(i)) + { + EXPECT_CALL(_lwipOutputSockets[i], send(Matcher<::etl::span const&>(_))) + .Times(3); + } + else + { + EXPECT_CALL(_lwipOutputSockets[i], send(Matcher<::etl::span const&>(_))) + .Times(0); + } + } + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.disable(); + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); +} + +/** + * \desc: Sending frames to active and disabled channels works with a bigger number of channels. + */ +TEST_F( + PduTransportIntegrationTest, + send_frames_to_active_and_disabled_channels_bigger_number_of_channels) +{ + constexpr uint8_t NUM_PDU_TRANSPORT_CHANNELS = ::routing::NUM_PDU_TRANSPORT_CHANNELS + 1; + constexpr size_t N = 16; + + ::etl::array pduTransportChannelIds; + ::etl::array<::udp::AbstractDatagramSocket*, NUM_PDU_TRANSPORT_CHANNELS> inputSockets; + ::etl::array<::udp::AbstractDatagramSocket*, NUM_PDU_TRANSPORT_CHANNELS> outputSockets; + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + pduTransportChannelIds[i] = _pduTransportChannelIds[i]; + inputSockets[i] = _inputSockets[i]; + outputSockets[i] = _outputSockets[i]; + } + pduTransportChannelIds.back() = _pduTransportChannelIds.back() + 1; + inputSockets.back() = nullptr; + outputSockets.back() = nullptr; + + new (&_pduTransportIntegration) PduTransportIntegration(); + + _pduTransportIntegration.init( + ::blob::CONFIGURATION_BLOB, pduTransportChannelIds, inputSockets, outputSockets); + + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + if (isTx(i)) + { + EXPECT_CALL(_lwipOutputSockets[i], send(Matcher<::etl::span const&>(_))) + .Times(3); + } + else + { + EXPECT_CALL(_lwipOutputSockets[i], send(Matcher<::etl::span const&>(_))) + .Times(0); + } + } + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.disable(); + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); +} + +/** + * \desc: Initialization succeeds even if some sockets are invalid. + */ +TEST_F(PduTransportIntegrationTest, init_invalid_sockets) +{ + // Invalid input sockets + { + new (&_pduTransportIntegration) PduTransportIntegration(); + + auto inputSockets = _inputSockets; + inputSockets.front() = nullptr; + inputSockets.back() = nullptr; + + _pduTransportIntegration.init( + ::blob::CONFIGURATION_BLOB, _pduTransportChannelIds, inputSockets, _outputSockets); + + EXPECT_EQ( + ::routing::NUM_PDU_TRANSPORT_CHANNELS, + _pduTransportIntegration.bufferedOutputWriters().size()); + } + + // Invalid output sockets + { + new (&_pduTransportIntegration) PduTransportIntegration(); + + auto outputSockets = _outputSockets; + outputSockets.front() = nullptr; + outputSockets.back() = nullptr; + + _pduTransportIntegration.init( + ::blob::CONFIGURATION_BLOB, _pduTransportChannelIds, _inputSockets, outputSockets); + + EXPECT_EQ( + ::routing::NUM_PDU_TRANSPORT_CHANNELS, + _pduTransportIntegration.bufferedOutputWriters().size()); + } +} + +/** + * \desc Activating and sending frames work with invalid sockets. + */ +TEST_F(PduTransportIntegrationTest, activate_and_send_with_invalid_sockets) +{ + constexpr size_t N = 16; + constexpr size_t FIRST = 0; + constexpr size_t LAST = ::routing::NUM_PDU_TRANSPORT_CHANNELS - 1; + + new (&_pduTransportIntegration) PduTransportIntegration(); + + auto inputSockets = _inputSockets; + auto outputSockets = _outputSockets; + inputSockets[FIRST] = nullptr; + inputSockets[LAST] = nullptr; + outputSockets[FIRST] = nullptr; + outputSockets[LAST] = nullptr; + + _pduTransportIntegration.init( + ::blob::CONFIGURATION_BLOB, _pduTransportChannelIds, inputSockets, outputSockets); + + size_t expectedReceivers = 0, expectedSenders = 0; + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + bool const validSockets = (i != FIRST) && (i != LAST); + if (validSockets && isRx(i)) + { + ++expectedReceivers; + } + if (validSockets && isTx(i)) + { + ++expectedSenders; + } + EXPECT_CALL(_lwipOutputSockets[i], send(Matcher<::etl::span const&>(_))) + .Times((validSockets && isTx(i)) ? 1 : 0); + } + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + + EXPECT_EQ(expectedReceivers, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(expectedSenders, _pduTransportIntegration.udpSenders().size()); + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.disable(); + EXPECT_EQ(0, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpSenders().size()); +} + +/** + * \desc: Activating and sending frames work with all sockets invalid. + */ +TEST_F(PduTransportIntegrationTest, activate_and_send_with_all_sockets_invalid) +{ + constexpr size_t N = 16; + + new (&_pduTransportIntegration) PduTransportIntegration(); + + auto inputSockets = _inputSockets; + auto outputSockets = _outputSockets; + inputSockets.fill(nullptr); + outputSockets.fill(nullptr); + + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + EXPECT_CALL(_lwipOutputSockets[i], send(Matcher<::etl::span const&>(_))) + .Times(0); + } + + _pduTransportIntegration.init( + ::blob::CONFIGURATION_BLOB, _pduTransportChannelIds, inputSockets, outputSockets); + + EXPECT_EQ( + ::routing::NUM_PDU_TRANSPORT_CHANNELS, + _pduTransportIntegration.bufferedOutputWriters().size()); + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + + EXPECT_EQ(0, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpSenders().size()); + + writeToAll(N); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.disable(); + EXPECT_EQ(0, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpSenders().size()); +} + +/** + * \desc: A channel is not activated or disabled if its sockets are invalid. + */ +TEST_F(PduTransportIntegrationTest, activate_and_disable_invalid_sockets) +{ + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + auto& inputSocket = _lwipInputSockets[i]; + EXPECT_CALL(inputSocket, bind(_, _)).Times(0); + EXPECT_CALL(inputSocket, join(_)).Times(0); + EXPECT_CALL(inputSocket, close()).Times(0); + + auto& outputSocket = _lwipOutputSockets[i]; + EXPECT_CALL(outputSocket, bind(_, _)).Times(0); + EXPECT_CALL(outputSocket, connect(_, _, _)).Times(0); + EXPECT_CALL(outputSocket, close()).Times(0); + EXPECT_CALL(outputSocket, disconnect()).Times(0); + } + + _inputSockets.fill(nullptr); + _outputSockets.fill(nullptr); + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + EXPECT_EQ(0, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpSenders().size()); + + _pduTransportIntegration.disable(); + EXPECT_EQ(0, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpSenders().size()); +} + +/** + * \desc: Data received on output sockets is discarded promptly. + */ +TEST_F(PduTransportIntegrationTest, discarding_listener) +{ + constexpr size_t LENGTH = 8; + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + if (isTx(i)) + { + auto& socket = _lwipOutputSockets[i]; + EXPECT_CALL(socket, read(nullptr, LENGTH)).Times(1); + + socket.getDataListener()->dataReceived(socket, _ipAddress, 0, _ipAddress, LENGTH); + } + } +} + +/** + * \desc: Checking transmission timeout of channels works. + */ +TEST_F(PduTransportIntegrationTest, check_transmission_timeouts) +{ + constexpr size_t N = 16; + constexpr uint32_t T = 10; + + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + if (isTx(i)) + { + EXPECT_CALL(_lwipOutputSockets[i], send(Matcher<::etl::span const&>(_))) + .Times(_transmissionTimeouts[i] == 0 ? (T + 1) : (T / _transmissionTimeouts[i])); + } + else + { + EXPECT_CALL(_lwipOutputSockets[i], send(Matcher<::etl::span const&>(_))) + .Times(0); + } + } + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + + commitToAll(N); + for (uint32_t i = 0; i < T; ++i, _timestamp = 1000U * i) + { + _pduTransportIntegration.checkTransmissionTimeouts(); + _pduTransportIntegration.sendUdpFrames(); + commitToAll(N); + } + _pduTransportIntegration.checkTransmissionTimeouts(); + _pduTransportIntegration.sendUdpFrames(); +} + +/** + * \desc: Calling methods without initialization does nothing. + */ +TEST_F(PduTransportIntegrationTest, methods_without_init) +{ + new (&_pduTransportIntegration) PduTransportIntegration(); + + for (size_t i = 0; i < ::routing::NUM_PDU_TRANSPORT_CHANNELS; ++i) + { + auto& inputSocket = _lwipInputSockets[i]; + EXPECT_CALL(inputSocket, bind(_, _)).Times(0); + EXPECT_CALL(inputSocket, join(_)).Times(0); + + EXPECT_CALL(inputSocket, close()).Times(0); + + auto& outputSocket = _lwipOutputSockets[i]; + EXPECT_CALL(outputSocket, bind(_, _)).Times(0); + EXPECT_CALL(outputSocket, connect(_, _, _)).Times(0); + + EXPECT_CALL(outputSocket, send(Matcher<::etl::span const&>(_))).Times(0); + + EXPECT_CALL(outputSocket, close()).Times(0); + EXPECT_CALL(outputSocket, disconnect()).Times(0); + } + + _pduTransportIntegration.activate(_ipAddress, _vlanId, {}); + EXPECT_EQ(0, _pduTransportIntegration.udpReceivers().size()); + EXPECT_EQ(0, _pduTransportIntegration.udpSenders().size()); + + _pduTransportIntegration.checkTransmissionTimeouts(); + _pduTransportIntegration.sendUdpFrames(); + + _pduTransportIntegration.disable(); +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/PduTransportRxAdapterTest.cpp b/libs/bsw/routing/test/src/routing/PduTransportRxAdapterTest.cpp new file mode 100644 index 00000000000..6b3e1249e57 --- /dev/null +++ b/libs/bsw/routing/test/src/routing/PduTransportRxAdapterTest.cpp @@ -0,0 +1,770 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/PduTransportRxAdapter.h" + +#include "routing/ErrorHandler.h" +#include "routing/definition.h" + +#include +#include +#include + +#include + +namespace +{ +using namespace ::testing; + +template +struct PduTransportRxAdapterTest +{ + using Queue = ::io::MemoryQueue; + using Reader = ::io::MemoryQueueReader; + using Writer = ::io::MemoryQueueWriter; + using RxAdapter = ::routing::PduTransportRxAdapter; + + PduTransportRxAdapterTest(::etl::span<::routing::Definition> defs) + : rxQueue() + , rxReader(rxQueue) + , rxWriter(rxQueue) + , invalidHeaderSizeCounter(0) + , unknownMessageIdCounter(0) + , invalidParsedLengthCounter(0) + , invalidMessageLengthCounter(0) + , invalidPayloadLengthCounter(0) + , errorHandler( + ::routing::ErrorHandler::Function::create< + PduTransportRxAdapterTest, + &PduTransportRxAdapterTest::handleError>(*this), + 0U) + { + routing::load(rxAdapterTable, defs, 0, rxAdapterTableMem); + (void)rxAdapter.emplace(rxReader, rxAdapterTable, errorHandler); + } + + void handleError(::routing::ErrorHandler::StatusCode const statusCode, uint8_t, uint32_t) + { + switch (statusCode) + { + case ::routing::ErrorHandler::StatusCode::INVALID_MESSAGE_HEADER_SIZE: + ++invalidHeaderSizeCounter; + break; + case ::routing::ErrorHandler::StatusCode::UNKNOWN_MESSAGE_ID: + ++unknownMessageIdCounter; + break; + case ::routing::ErrorHandler::StatusCode::INVALID_PARSED_LENGTH: + ++invalidParsedLengthCounter; + break; + case ::routing::ErrorHandler::StatusCode::INVALID_MESSAGE_LENGTH: + ++invalidMessageLengthCounter; + break; + case ::routing::ErrorHandler::StatusCode::INVALID_PAYLOAD_LENGTH: + ++invalidPayloadLengthCounter; + break; + default: break; + } + } + + Queue rxQueue; + Reader rxReader; + Writer rxWriter; + + ::routing::RxAdapterTableMem rxAdapterTableMem; + ::routing::RxAdapterTable rxAdapterTable; + + ::etl::optional rxAdapter; + + uint32_t invalidHeaderSizeCounter; + uint32_t unknownMessageIdCounter; + uint32_t invalidParsedLengthCounter; + uint32_t invalidMessageLengthCounter; + uint32_t invalidPayloadLengthCounter; + + ::routing::ErrorHandler errorHandler; +}; + +::etl::span writePdu(::etl::span& frame, uint32_t const id, size_t const size) +{ + auto const pdu = frame.size() > size ? frame.first(size) : frame; + if (pdu.size() >= ::routing::RxAdapter::MESSAGE_HEADER_SIZE) + { + auto header = pdu.first(::routing::RxAdapter::MESSAGE_HEADER_SIZE); + header.take<::etl::be_uint32_t>() = id; + auto& length = header.take<::etl::be_uint32_t>(); + length = size - ::routing::RxAdapter::MESSAGE_HEADER_SIZE; + } + frame.advance(size); + return pdu; +} + +::etl::span allocateFrame(uint32_t const id, size_t const size, ::io::IWriter& writer) +{ + auto frame = writer.allocate(size); + if (!frame.empty()) + { + ::etl::span pdu = frame; + writePdu(pdu, id, size); + } + return frame; +} + +void writeFrame(uint32_t const id, size_t const size, ::io::IWriter& writer) +{ + auto frame = allocateFrame(id, size, writer); + if (!frame.empty()) + { + writer.commit(); + } +} + +/** + * \desc + * The max_size function returns the correct value. + */ +TEST(PduTransportRxAdapterTest, max_size) +{ + constexpr size_t CAPACITY = 128; + constexpr size_t MAX_ELEMENT_SIZE = 16; + + PduTransportRxAdapterTest rxAdapterTest({}); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + EXPECT_EQ(rxAdapter.maxSize(), MAX_ELEMENT_SIZE); +} + +/** + * \desc + * A single PDU is extracted out of a single frame with one message. + */ +TEST(PduTransportRxAdapterTest, extract_pdu_from_one_frame_with_one_message) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint32_t OFFSET = 0; + constexpr uint32_t PAYLOAD_LENGTH = 8; + constexpr uint32_t PDU_ID = 0; + uint8_t expectedPayload[PAYLOAD_LENGTH] = {0, 1, 2, 3, 4, 5, 6, 7}; + + ::routing::Definition defs[] + = {::routing::Definition().in(0, MESSAGE_ID, OFFSET, PAYLOAD_LENGTH)}; + PduTransportRxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + + auto message = allocateFrame(MESSAGE_ID, size, rxWriter); + EXPECT_TRUE(::etl::copy( + ::etl::make_span(expectedPayload), + message.subspan(::routing::RxAdapter::MESSAGE_HEADER_SIZE))); + rxWriter.commit(); + + auto const pdu = rxAdapter.peek(); + EXPECT_EQ(size, pdu.size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(size, rxAdapter.byteCounter()); + EXPECT_EQ(0, rxAdapter.discardedPduCounter()); + EXPECT_EQ(0, rxAdapter.discardedByteCounter()); + + auto res = pdu; + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(PDU_ID, id); + + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(PAYLOAD_LENGTH, l); + EXPECT_THAT(res, ElementsAreArray(expectedPayload)); + EXPECT_THAT(rxAdapter.peek(), ElementsAreArray(pdu)); + + rxAdapter.release(); + EXPECT_EQ(0, rxAdapter.peek().size()); +} + +/** + * \desc + * Multiple PDUs are extracted out of a single frame with one message. + */ +TEST(PduTransportRxAdapterTest, extract_multiple_pdus_from_one_frame_with_one_message) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint8_t PAYLOAD_LENGTH = 8; + uint8_t expectedPayload[PAYLOAD_LENGTH] = {0, 1, 2, 3, 4, 5, 6, 7}; + + constexpr size_t NUMBER_OF_PDUS = 3; + uint32_t offsets[NUMBER_OF_PDUS] = {0, 0, 4}; + uint32_t payloadLengths[NUMBER_OF_PDUS] = {8, 4, 4}; + + ::routing::Definition defs[] + = {::routing::Definition().in(0, MESSAGE_ID, offsets[0], payloadLengths[0]), + ::routing::Definition().in(0, MESSAGE_ID, offsets[1], payloadLengths[1]), + ::routing::Definition().in(0, MESSAGE_ID, offsets[2], payloadLengths[2])}; + PduTransportRxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + auto message = allocateFrame(MESSAGE_ID, size, rxWriter); + ::etl::copy( + ::etl::make_span(expectedPayload), + message.subspan(::routing::RxAdapter::MESSAGE_HEADER_SIZE)); + rxWriter.commit(); + + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + auto const pdu = rxAdapter.peek(); + EXPECT_EQ(::routing::RxAdapter::MESSAGE_HEADER_SIZE + payloadLengths[i], pdu.size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(size, rxAdapter.byteCounter()); + EXPECT_EQ(0, rxAdapter.discardedPduCounter()); + EXPECT_EQ(0, rxAdapter.discardedByteCounter()); + + auto res = pdu; + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(i, id); + + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(payloadLengths[i], l); + EXPECT_THAT( + res, + ElementsAreArray( + ::etl::make_span(expectedPayload).subspan(offsets[i], payloadLengths[i]))); + EXPECT_THAT(rxAdapter.peek(), ElementsAreArray(pdu)); + + rxAdapter.release(); + } + + EXPECT_EQ(0, rxAdapter.peek().size()); +} + +/** + * \desc + * Multiple PDUs are extracted out of a single frame with multiple messages. + */ +TEST(PduTransportRxAdapterTest, extract_multiple_pdus_from_one_frame_with_multiple_messages) +{ + constexpr size_t NUMBER_OF_MESSAGES = 3; + constexpr size_t NUMBER_OF_PDUS = 6; + constexpr size_t MAX_PAYLOAD_LENGTH = 8; + + uint32_t messageIds[NUMBER_OF_PDUS] = {1, 2, 2, 3, 3, 3}; + uint32_t offsets[NUMBER_OF_PDUS] = {0, 0, 4, 0, 4, 0}; + uint32_t payloadLengths[NUMBER_OF_PDUS] = {8, 8, 4, 8, 4, 4}; + uint8_t expectedPayload[MAX_PAYLOAD_LENGTH] = {0, 1, 2, 3, 4, 5, 6, 7}; + uint32_t expectedPduCounter = 0U, expectedByteCounter = 0U; + + ::etl::vector<::routing::Definition, NUMBER_OF_PDUS> defs; + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + defs.emplace_back( + ::routing::Definition().in(0, messageIds[i], offsets[i], payloadLengths[i])); + } + + PduTransportRxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + MAX_PAYLOAD_LENGTH; + + uint32_t messageId = 0; + auto frame = rxWriter.allocate(NUMBER_OF_MESSAGES * size); + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + if (messageId != messageIds[i]) + { + messageId = messageIds[i]; + auto message = writePdu(frame, messageId, size); + ::etl::copy( + ::etl::make_span(expectedPayload), + message.subspan(::routing::RxAdapter::MESSAGE_HEADER_SIZE)); + } + } + rxWriter.commit(); + + messageId = 0; + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + if (messageId != messageIds[i]) + { + messageId = messageIds[i]; + ++expectedPduCounter; + expectedByteCounter += size; + } + + auto const payloadLength = payloadLengths[i]; + auto const offset = offsets[i]; + + auto const pdu = rxAdapter.peek(); + EXPECT_EQ(pdu.size(), ::routing::RxAdapter::MESSAGE_HEADER_SIZE + payloadLength); + EXPECT_EQ(expectedPduCounter, rxAdapter.pduCounter()); + EXPECT_EQ(expectedByteCounter, rxAdapter.byteCounter()); + EXPECT_EQ(rxAdapter.discardedPduCounter(), 0); + EXPECT_EQ(rxAdapter.discardedByteCounter(), 0); + + auto res = pdu; + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(i, id); + + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(payloadLength, l); + + EXPECT_THAT( + res, + ElementsAreArray(::etl::make_span(expectedPayload).subspan(offset, payloadLength))); + EXPECT_THAT(rxAdapter.peek(), ElementsAreArray(pdu)); + + rxAdapter.release(); + } + + EXPECT_EQ(0, rxAdapter.peek().size()); +} + +/** + * \desc + * Multiple PDUs are extracted out of multiple frames with one message each. + */ +TEST(PduTransportRxAdapterTest, extract_multiple_pdus_from_multiple_frames_with_one_message_each) +{ + constexpr size_t NUMBER_OF_PDUS = 6; + constexpr size_t MAX_PAYLOAD_LENGTH = 8; + + uint32_t messageIds[NUMBER_OF_PDUS] = {1, 2, 2, 3, 3, 3}; + uint32_t offsets[NUMBER_OF_PDUS] = {0, 0, 4, 0, 4, 0}; + uint32_t payloadLengths[NUMBER_OF_PDUS] = {8, 8, 4, 8, 4, 4}; + uint8_t expectedPayload[MAX_PAYLOAD_LENGTH] = {0, 1, 2, 3, 4, 5, 6, 7}; + uint32_t expectedPduCounter = 0U, expectedByteCounter = 0U; + + ::etl::vector<::routing::Definition, NUMBER_OF_PDUS> defs; + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + defs.emplace_back( + ::routing::Definition().in(0, messageIds[i], offsets[i], payloadLengths[i])); + } + + PduTransportRxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + MAX_PAYLOAD_LENGTH; + uint32_t messageId = 0; + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + if (messageId != messageIds[i]) + { + messageId = messageIds[i]; + auto frame = rxWriter.allocate(size); + auto message = writePdu(frame, messageId, size); + ::etl::copy( + ::etl::make_span(expectedPayload), + message.subspan(::routing::RxAdapter::MESSAGE_HEADER_SIZE)); + rxWriter.commit(); + } + } + + messageId = 0; + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + if (messageId != messageIds[i]) + { + messageId = messageIds[i]; + ++expectedPduCounter; + expectedByteCounter += size; + } + + auto const payloadLength = payloadLengths[i]; + auto const offset = offsets[i]; + + auto const pdu = rxAdapter.peek(); + EXPECT_EQ(pdu.size(), ::routing::RxAdapter::MESSAGE_HEADER_SIZE + payloadLength); + EXPECT_EQ(expectedPduCounter, rxAdapter.pduCounter()); + EXPECT_EQ(expectedByteCounter, rxAdapter.byteCounter()); + EXPECT_EQ(rxAdapter.discardedPduCounter(), 0); + EXPECT_EQ(rxAdapter.discardedByteCounter(), 0); + + auto res = pdu; + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(i, id); + + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(payloadLength, l); + + EXPECT_THAT( + res, + ElementsAreArray(::etl::make_span(expectedPayload).subspan(offset, payloadLength))); + EXPECT_THAT(rxAdapter.peek(), ElementsAreArray(pdu)); + + rxAdapter.release(); + } + + EXPECT_EQ(0, rxAdapter.peek().size()); +} + +/** + * \desc + * Multiple PDUs are extracted out of multiple frames with multiple messages each. + */ +TEST( + PduTransportRxAdapterTest, + extract_multiple_pdus_multiple_from_frames_with_multiple_messages_each) +{ + constexpr size_t NUMBER_OF_FRAMES = 3; + constexpr size_t NUMBER_OF_MESSAGES = 3; + constexpr size_t NUMBER_OF_PDUS = 6; + constexpr size_t MAX_PAYLOAD_LENGTH = 8; + + uint32_t messageIds[NUMBER_OF_PDUS] = {1, 2, 2, 3, 3, 3}; + uint32_t offsets[NUMBER_OF_PDUS] = {0, 0, 4, 0, 4, 0}; + uint32_t payloadLengths[NUMBER_OF_PDUS] = {8, 8, 4, 8, 4, 4}; + uint8_t expectedPayload[MAX_PAYLOAD_LENGTH] = {0, 1, 2, 3, 4, 5, 6, 7}; + uint32_t expectedPduCounter = 0U, expectedByteCounter = 0U; + + ::etl::vector<::routing::Definition, NUMBER_OF_PDUS> defs; + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + defs.emplace_back( + ::routing::Definition().in(0, messageIds[i], offsets[i], payloadLengths[i])); + } + + PduTransportRxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + MAX_PAYLOAD_LENGTH; + + uint32_t messageId = 0; + auto messages = rxWriter.allocate(NUMBER_OF_MESSAGES * size); + auto frame = messages; + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + if (messageId != messageIds[i]) + { + messageId = messageIds[i]; + auto message = writePdu(frame, messageId, size); + ::etl::copy( + ::etl::make_span(expectedPayload), + message.subspan(::routing::RxAdapter::MESSAGE_HEADER_SIZE)); + } + } + + for (size_t i = 0; i < NUMBER_OF_FRAMES; ++i) + { + auto frame = rxWriter.allocate(messages.size()); + ::etl::copy(messages, frame); + rxWriter.commit(); + } + + for (size_t j = 0; j < NUMBER_OF_FRAMES; ++j) + { + messageId = 0; + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + if (messageId != messageIds[i]) + { + messageId = messageIds[i]; + ++expectedPduCounter; + expectedByteCounter += size; + } + + auto const payloadLength = payloadLengths[i]; + auto const offset = offsets[i]; + + auto const pdu = rxAdapter.peek(); + EXPECT_EQ(pdu.size(), ::routing::RxAdapter::MESSAGE_HEADER_SIZE + payloadLength); + EXPECT_EQ(expectedPduCounter, rxAdapter.pduCounter()); + EXPECT_EQ(expectedByteCounter, rxAdapter.byteCounter()); + EXPECT_EQ(rxAdapter.discardedPduCounter(), 0); + EXPECT_EQ(rxAdapter.discardedByteCounter(), 0); + + auto res = pdu; + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(i, id); + + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(payloadLength, l); + + EXPECT_THAT( + res, + ElementsAreArray(::etl::make_span(expectedPayload).subspan(offset, payloadLength))); + EXPECT_THAT(rxAdapter.peek(), ElementsAreArray(pdu)); + + rxAdapter.release(); + } + } + EXPECT_EQ(0, rxAdapter.peek().size()); +} + +/** + * \desc + * PDU is dropped if message header size is invalid. + */ +TEST(PduTransportRxAdapterTest, discard_pdu_invalid_message_header_size) +{ + constexpr uint32_t MESSAGE_ID = 1; + + ::routing::Definition defs[] = {::routing::Definition().in(0, MESSAGE_ID, 0, 8)}; + PduTransportRxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE - 1; + writeFrame(MESSAGE_ID, size, rxWriter); + + EXPECT_EQ(0, rxAdapter.peek().size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(size, rxAdapter.byteCounter()); + EXPECT_EQ(1, rxAdapter.discardedPduCounter()); + EXPECT_EQ(size, rxAdapter.discardedByteCounter()); + EXPECT_EQ(1, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(1, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(0, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(0, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(0, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(0, rxAdapterTest.invalidPayloadLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidPayloadLengthPdus()); +} + +/** + * \desc + * PDU is dropped if message ID is unknown. + */ +TEST(PduTransportRxAdapterTest, discard_pdu_unknown_message_id) +{ + constexpr size_t NUMBER_OF_PDUS = 3; + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint32_t PAYLOAD_LENGTH = 8; + + ::routing::Definition defs[] = {::routing::Definition().in(0, MESSAGE_ID, 0, PAYLOAD_LENGTH)}; + PduTransportRxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + + writeFrame(MESSAGE_ID + 1, size, rxWriter); + auto frame = rxWriter.allocate(2 * size); + writePdu(frame, MESSAGE_ID + 1, size); + writePdu(frame, MESSAGE_ID + 1, size); + rxWriter.commit(); + + EXPECT_EQ(0, rxAdapter.peek().size()); + EXPECT_EQ(NUMBER_OF_PDUS, rxAdapter.pduCounter()); + EXPECT_EQ(NUMBER_OF_PDUS * size, rxAdapter.byteCounter()); + EXPECT_EQ(NUMBER_OF_PDUS, rxAdapter.discardedPduCounter()); + EXPECT_EQ(NUMBER_OF_PDUS * size, rxAdapter.discardedByteCounter()); + EXPECT_EQ(0, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(0, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(NUMBER_OF_PDUS, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(NUMBER_OF_PDUS, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(0, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(0, rxAdapterTest.invalidPayloadLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidPayloadLengthPdus()); +} + +/** + * \desc + * PDU is dropped if parsed length is invalid. + */ +TEST(PduTransportRxAdapterTest, discard_pdu_invalid_parsed_length) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint32_t PAYLOAD_LENGTH = 8; + + ::routing::Definition defs[] = {::routing::Definition().in(0, MESSAGE_ID, 0, PAYLOAD_LENGTH)}; + PduTransportRxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + auto pdu = rxWriter.allocate(size); + pdu.take<::etl::be_uint32_t>() = MESSAGE_ID; + pdu.take<::etl::be_uint32_t>() = PAYLOAD_LENGTH + 1; + rxWriter.commit(); + + EXPECT_EQ(0, rxAdapter.peek().size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(size, rxAdapter.byteCounter()); + EXPECT_EQ(1, rxAdapter.discardedPduCounter()); + EXPECT_EQ(size, rxAdapter.discardedByteCounter()); + EXPECT_EQ(0, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(0, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(0, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(0, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(1, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(1, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(0, rxAdapterTest.invalidPayloadLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidPayloadLengthPdus()); +} + +/** + * \desc + * PDU is discarded if payload length is invalid. If at least one PDU is extracted out of a message, + * it is not discarded. + */ +TEST(PduTransportRxAdapterTest, discard_pdu_invalid_payload_length) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint8_t PAYLOAD_LENGTH = 4; + + ::routing::Definition defs[] + = {::routing::Definition().in(0, MESSAGE_ID, 0, PAYLOAD_LENGTH), + ::routing::Definition().in(0, MESSAGE_ID, 0, PAYLOAD_LENGTH + 1)}; + PduTransportRxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const validSize = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + auto const invalidSize = validSize - 1; + + writeFrame(MESSAGE_ID, validSize, rxWriter); + writeFrame(MESSAGE_ID, invalidSize, rxWriter); + auto frame = rxWriter.allocate(validSize + invalidSize); + writePdu(frame, MESSAGE_ID, validSize); + writePdu(frame, MESSAGE_ID, invalidSize); + rxWriter.commit(); + + EXPECT_EQ(validSize, rxAdapter.peek().size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(rxAdapter.byteCounter(), validSize); + EXPECT_EQ(0, rxAdapter.discardedPduCounter()); + EXPECT_EQ(0, rxAdapter.discardedByteCounter()); + EXPECT_EQ(0, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(0, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(0, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(0, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(0, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(0, rxAdapterTest.invalidPayloadLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidPayloadLengthPdus()); + + rxAdapter.release(); + + EXPECT_EQ(validSize, rxAdapter.peek().size()); + EXPECT_EQ(3, rxAdapter.pduCounter()); + EXPECT_EQ(2 * validSize + invalidSize, rxAdapter.byteCounter()); + EXPECT_EQ(1, rxAdapter.discardedPduCounter()); + EXPECT_EQ(invalidSize, rxAdapter.discardedByteCounter()); + EXPECT_EQ(0, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(0, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(0, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(0, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(0, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(1, rxAdapterTest.invalidPayloadLengthCounter); + EXPECT_EQ(1, rxAdapter.invalidPayloadLengthPdus()); + + rxAdapter.release(); + + EXPECT_EQ(0, rxAdapter.peek().size()); + EXPECT_EQ(4, rxAdapter.pduCounter()); + EXPECT_EQ(2 * (validSize + invalidSize), rxAdapter.byteCounter()); + EXPECT_EQ(2, rxAdapter.discardedPduCounter()); + EXPECT_EQ(2 * invalidSize, rxAdapter.discardedByteCounter()); + EXPECT_EQ(0, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(0, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(0, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(0, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(0, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(2, rxAdapterTest.invalidPayloadLengthCounter); + EXPECT_EQ(2, rxAdapter.invalidPayloadLengthPdus()); +} + +/** + * \desc + * PDU is discarded if message length is invalid. + */ +TEST(PduTransportRxAdapterTest, discard_pdu_invalid_message_length) +{ + constexpr uint32_t NUMBER_OF_PDUS = 4; + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint8_t PAYLOAD_LENGTH = 8; + + ::routing::Definition defs[] + = {::routing::Definition().in(0, MESSAGE_ID, PAYLOAD_LENGTH, 0, PAYLOAD_LENGTH), + ::routing::Definition().in(0, MESSAGE_ID, PAYLOAD_LENGTH, 0, PAYLOAD_LENGTH - 1)}; + PduTransportRxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + constexpr size_t size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + + writeFrame(MESSAGE_ID, size + 1, rxWriter); + writeFrame(MESSAGE_ID, size - 1, rxWriter); + auto frame = rxWriter.allocate(2 * size); + writePdu(frame, MESSAGE_ID, size + 1); + writePdu(frame, MESSAGE_ID, size - 1); + rxWriter.commit(); + + EXPECT_EQ(0, rxAdapter.peek().size()); + EXPECT_EQ(NUMBER_OF_PDUS, rxAdapter.pduCounter()); + EXPECT_EQ(NUMBER_OF_PDUS * size, rxAdapter.byteCounter()); + EXPECT_EQ(NUMBER_OF_PDUS, rxAdapter.discardedPduCounter()); + EXPECT_EQ(NUMBER_OF_PDUS * size, rxAdapter.discardedByteCounter()); + EXPECT_EQ(0, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(0, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(0, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(0, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(0, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(NUMBER_OF_PDUS, rxAdapterTest.invalidMessageLengthCounter); + EXPECT_EQ(NUMBER_OF_PDUS, rxAdapter.invalidMessageLengthPdus()); +} + +/** + * \desc + * Invalid PDUs are discarded before extracting one from a message. + */ +TEST(PduTransportRxAdapterTest, discard_invalid_pdus) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint8_t PAYLOAD_LENGTH = 8; + + ::routing::Definition defs[] + = {::routing::Definition().in(0, MESSAGE_ID, PAYLOAD_LENGTH, 0, PAYLOAD_LENGTH), + ::routing::Definition().in(0, MESSAGE_ID + 1, 0, PAYLOAD_LENGTH)}; + PduTransportRxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + uint32_t expectedPduCounter = 0U, expectedByteCounter = 0U, expectedDiscardedPduCounter = 0U, + expectedDiscardedByteCounter = 0U; + + constexpr size_t size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + // invalid header size + writeFrame(MESSAGE_ID, ::routing::RxAdapter::MESSAGE_HEADER_SIZE - 1, rxWriter); + // invalid parsed length + auto frame = rxWriter.allocate(size); + frame.take<::etl::be_uint32_t>() = MESSAGE_ID; + frame.take<::etl::be_uint32_t>() = PAYLOAD_LENGTH + 1; + rxWriter.commit(); + // unknown message ID + frame = rxWriter.allocate(4 * size); + writePdu(frame, MESSAGE_ID + 2, size); + // invalid message length + writePdu(frame, MESSAGE_ID, size - 1); + // invalid payload length + writePdu(frame, MESSAGE_ID + 1, size - 1); + + writePdu(frame, MESSAGE_ID, ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH); + rxWriter.commit(); + + auto pdu = rxAdapter.peek(); + + expectedPduCounter += 6; + expectedByteCounter += 85; + expectedDiscardedPduCounter += 5; + expectedDiscardedByteCounter += 69; + + EXPECT_EQ(::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH, pdu.size()); + EXPECT_EQ(expectedPduCounter, rxAdapter.pduCounter()); + EXPECT_EQ(expectedByteCounter, rxAdapter.byteCounter()); + EXPECT_EQ(expectedDiscardedPduCounter, rxAdapter.discardedPduCounter()); + EXPECT_EQ(expectedDiscardedByteCounter, rxAdapter.discardedByteCounter()); +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/PduTransportTxAdapterTest.cpp b/libs/bsw/routing/test/src/routing/PduTransportTxAdapterTest.cpp new file mode 100644 index 00000000000..6e602affbfb --- /dev/null +++ b/libs/bsw/routing/test/src/routing/PduTransportTxAdapterTest.cpp @@ -0,0 +1,265 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/PduTransportTxAdapter.h" + +#include +#include +#include + +#include + +namespace +{ +using namespace ::testing; + +template +struct PduTransportTxAdapterTest +{ + using Queue = ::io::MemoryQueue; + using Reader = ::io::MemoryQueueReader; + using Writer = ::io::MemoryQueueWriter; + + PduTransportTxAdapterTest() + : txQueue() + , txReader(txQueue) + , txWriter(txQueue) + , memAllocationFailureCounter(0) + , errorHandler( + ::routing::ErrorHandler::Function::create< + PduTransportTxAdapterTest, + &PduTransportTxAdapterTest::handleError>(*this), + 0U) + , txAdapter(txWriter, errorHandler) + {} + + void handleError(::routing::ErrorHandler::StatusCode const statusCode, uint8_t, uint32_t) + { + switch (statusCode) + { + case ::routing::ErrorHandler::StatusCode::MEM_ALLOCATION_FAILURE: + ++memAllocationFailureCounter; + break; + default: break; + } + } + + Queue txQueue; + Reader txReader; + Writer txWriter; + + uint32_t memAllocationFailureCounter; + + ::routing::ErrorHandler errorHandler; + + ::routing::PduTransportTxAdapter txAdapter; +}; + +::etl::span allocatePdu( + uint32_t const id, size_t const size, ::io::IWriter& writer, uint8_t const pattern = 0xAB) +{ + auto pdu = writer.allocate(size); + if (!pdu.empty()) + { + auto data = pdu; + data.take<::etl::be_uint32_t>() = id; + auto& length = data.take<::etl::be_uint32_t>(); + length = data.size(); + ::etl::fill(data.begin(), data.end(), pattern); + } + return pdu; +} + +void writePdu( + uint32_t const id, size_t const size, ::io::IWriter& writer, uint8_t const pattern = 0xAB) +{ + auto pdu = allocatePdu(id, size, writer, pattern); + if (!pdu.empty()) + { + writer.commit(); + } +} + +/** + * \desc + * The max_size function returns the correct value. + */ +TEST(PduTransportTxAdapterTest, max_size) +{ + constexpr size_t CAPACITY = 128; + constexpr size_t MAX_ELEMENT_SIZE = 16; + + PduTransportTxAdapterTest txAdapterTest; + auto& txAdapter = txAdapterTest.txAdapter; + EXPECT_EQ(MAX_ELEMENT_SIZE, txAdapter.maxSize()); +} + +/** + * \desc + * Commiting without allocation does nothing. + */ +TEST(PduTransportTxAdapterTest, commit_without_allocation) +{ + constexpr size_t CAPACITY = 128; + constexpr size_t MAX_ELEMENT_SIZE = 16; + + PduTransportTxAdapterTest txAdapterTest; + auto& txReader = txAdapterTest.txReader; + auto& txAdapter = txAdapterTest.txAdapter; + + txAdapter.commit(); + + EXPECT_EQ(0, txReader.peek().size()); + EXPECT_EQ(0, txAdapter.pduCounter()); + EXPECT_EQ(0, txAdapter.byteCounter()); + EXPECT_EQ(0, txAdapter.droppedPduCounter()); + EXPECT_EQ(0, txAdapter.droppedByteCounter()); +} + +/** + * \desc + * Sending a PDU through the PduTransportTxAdapter doesn't modify it. + */ +TEST(PduTransportTxAdapterTest, allocate_commit) +{ + constexpr size_t SIZE = 16; + PduTransportTxAdapterTest<> txAdapterTest; + ::etl::vector sentPdu(SIZE); + + auto& txReader = txAdapterTest.txReader; + auto& txAdapter = txAdapterTest.txAdapter; + + ::etl::copy(allocatePdu(0xAA, SIZE, txAdapter), ::etl::make_span(sentPdu)); + txAdapter.commit(); + + EXPECT_EQ(SIZE, txReader.peek().size()); + EXPECT_THAT(txReader.peek(), ElementsAreArray(sentPdu)); + EXPECT_EQ(1, txAdapter.pduCounter()); + EXPECT_EQ(SIZE, txAdapter.byteCounter()); + EXPECT_EQ(0, txAdapter.droppedPduCounter()); + EXPECT_EQ(0, txAdapter.droppedByteCounter()); +} + +/** + * \desc + * PDU is dropped on allocation if queue is full. + */ +TEST(PduTransportTxAdapterTest, drop_pdu_on_allocation_queue_is_full) +{ + constexpr size_t MAX_ELEMENT_SIZE = 16; + constexpr size_t CAPACITY = 32; + + PduTransportTxAdapterTest txAdapterTest; + auto& txAdapter = txAdapterTest.txAdapter; + + writePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + + auto pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + + EXPECT_EQ(0, pdu.size()); + EXPECT_EQ(1, txAdapter.pduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE, txAdapter.byteCounter()); + EXPECT_EQ(1, txAdapter.droppedPduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE, txAdapter.droppedByteCounter()); + EXPECT_EQ(1, txAdapterTest.memAllocationFailureCounter); + EXPECT_EQ(1, txAdapter.failedMemAllocPdus()); +} + +/** + * \desc + * PDU is dropped on allocation if message size is greater than max element size of queue. + */ +TEST(PduTransportTxAdapterTest, drop_pdu_on_allocation_message_is_too_big) +{ + constexpr size_t MAX_ELEMENT_SIZE = 16; + constexpr size_t CAPACITY = 32; + + PduTransportTxAdapterTest txAdapterTest; + auto& txReader = txAdapterTest.txReader; + auto& txAdapter = txAdapterTest.txAdapter; + + auto pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE + 1, txAdapter); + txAdapter.commit(); + + EXPECT_EQ(0, pdu.size()); + EXPECT_EQ(0, txReader.peek().size()); + EXPECT_EQ(0, txAdapter.pduCounter()); + EXPECT_EQ(0, txAdapter.byteCounter()); + EXPECT_EQ(1, txAdapter.droppedPduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE + 1, txAdapter.droppedByteCounter()); + EXPECT_EQ(1, txAdapterTest.memAllocationFailureCounter); + EXPECT_EQ(1, txAdapter.failedMemAllocPdus()); +} + +/** + * \desc Multiple consecutive drops only cause the error handling to be triggered once. + */ +TEST(PduTransportTxAdapterTest, multiple_drops_one_error_handling) +{ + constexpr size_t MAX_ELEMENT_SIZE = 16; + constexpr size_t CAPACITY = 32; + + PduTransportTxAdapterTest txAdapterTest; + auto& txAdapter = txAdapterTest.txAdapter; + + writePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + + auto pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + + EXPECT_EQ(0, pdu.size()); + EXPECT_EQ(1, txAdapter.pduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE, txAdapter.byteCounter()); + EXPECT_EQ(1, txAdapter.droppedPduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE, txAdapter.droppedByteCounter()); + EXPECT_EQ(1, txAdapterTest.memAllocationFailureCounter); + EXPECT_EQ(1, txAdapter.failedMemAllocPdus()); + + pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + + EXPECT_EQ(0, pdu.size()); + EXPECT_EQ(1, txAdapter.pduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE, txAdapter.byteCounter()); + EXPECT_EQ(2, txAdapter.droppedPduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE * 2, txAdapter.droppedByteCounter()); + EXPECT_EQ(1, txAdapterTest.memAllocationFailureCounter); // only one call + EXPECT_EQ(2, txAdapter.failedMemAllocPdus()); + + // Succesfully allocating a PDU then failing counts again + auto reader = txAdapterTest.txReader; + reader.peek(); + reader.release(); + writePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + pdu = allocatePdu(0x1, MAX_ELEMENT_SIZE, txAdapter); + + EXPECT_EQ(0, pdu.size()); + EXPECT_EQ(2, txAdapter.pduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE * 2, txAdapter.byteCounter()); + EXPECT_EQ(3, txAdapter.droppedPduCounter()); + EXPECT_EQ(MAX_ELEMENT_SIZE * 3, txAdapter.droppedByteCounter()); + EXPECT_EQ(2, txAdapterTest.memAllocationFailureCounter); // this one is counted + EXPECT_EQ(3, txAdapter.failedMemAllocPdus()); +} + +/** + * \desc + * Flush triggers the output writer's flush method. + */ +TEST(PduTransportTxAdapterTest, flush) +{ + ::io::IWriterMock writerMock; + + EXPECT_CALL(writerMock, flush()).Times(1); + + ::routing::PduTransportTxAdapter txAdapter(writerMock, {}); + + txAdapter.flush(); +} +} // namespace diff --git a/libs/bsw/routing/test/src/routing/RouterTest.cpp b/libs/bsw/routing/test/src/routing/RouterTest.cpp new file mode 100644 index 00000000000..63950d16bc0 --- /dev/null +++ b/libs/bsw/routing/test/src/routing/RouterTest.cpp @@ -0,0 +1,706 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/Router.h" + +#include "routing/definition.h" + +#include "routing/PduRoutingTable.h" + +#include +#include +#include +#include +#include + +#include + +namespace +{ +using namespace ::testing; + +template +struct RouterWithDefinitions +{ + static constexpr size_t CAPACITY = 1024; + static constexpr size_t MAX_ELEMENT_SIZE = 128; + + using Router = ::routing::Router; + + using Queue = ::io::MemoryQueue; + using Reader = ::io::MemoryQueueReader; + using Writer = ::io::MemoryQueueWriter; + + RouterWithDefinitions() + { + for (size_t i = 0U; i < MAX_NUM_CHANNELS; ++i) + { + auto& rxQueue = rxQueues.emplace_back(); + auto& txQueue = txQueues.emplace_back(); + (void)rxReaders.emplace_back(rxQueue); + (void)txReaders.emplace_back(txQueue); + (void)rxWriters.emplace_back(rxQueue); + (void)txWriters.emplace_back(txQueue); + + auto& reader = rxReaders[i]; + readers[i] = &reader; + auto& writer = txWriters[i]; + writers[i] = &writer; + } + } + + void init( + ::etl::span<::routing::Definition> defs, + ::etl::span<::io::IReader*> readers, + ::etl::span<::io::IWriter*> writers) + { + ::routing::load(pduRoutingTable, defs, routingMem); + router.init(pduRoutingTable, readers, writers); + } + + void init(::etl::span<::routing::Definition> defs) { init(defs, readers, writers); } + + void init(::etl::span const config) { router.init(config, readers, writers); } + + bool run() { return router.run(); } + + Router router; + + ::etl::vector rxQueues; + ::etl::vector txQueues; + ::etl::vector rxReaders; + ::etl::vector txReaders; + ::etl::vector rxWriters; + ::etl::vector txWriters; + ::etl::array<::io::IReader*, MAX_NUM_CHANNELS> readers; + ::etl::array<::io::IWriter*, MAX_NUM_CHANNELS> writers; + + ::routing::RoutingTableMem routingMem; + ::routing::PduRoutingTable pduRoutingTable; +}; + +void writePdu(uint32_t const id, size_t const size, ::io::IWriter& writer) +{ + auto pdu = writer.allocate(size); + if (!pdu.empty() && pdu.size() >= 2 * sizeof(::etl::be_uint32_t)) + { + pdu.take<::etl::be_uint32_t>() = id; + auto& length = pdu.take<::etl::be_uint32_t>(); + length = pdu.size(); + writer.commit(); + } +} + +/** + * \desc + * Routing a single PDU from a source channel to an output channel works + */ +TEST(RouterTest, route_single_pdu) +{ + constexpr size_t MAX_NUM_CHANNELS = 2; + ::routing::Definition defs[] = {::routing::Definition().in(0, 0x34, 0, 4).out(1, 0x43, 0, 4)}; + RouterWithDefinitions router; + router.init(defs); + + // We need to use internalPduId, so 0 + writePdu(0U, 12, router.rxWriters[0]); + + ASSERT_TRUE(router.run()); + ASSERT_EQ(0, router.txReaders[0].peek().size()); + ASSERT_EQ(12, router.txReaders[1].peek().size()); // id + length + payload size + EXPECT_EQ(0x43U, router.txReaders[1].peek().take<::etl::be_uint32_t>()); + router.txReaders[1].release(); +} + +/** + * \desc + * PDU is discarded and not routed to any channel if its size is invalid + */ +TEST(RouterTest, discard_pdu_invalid_size) +{ + constexpr size_t MAX_NUM_CHANNELS = 2; + ::routing::Definition defs[] = {::routing::Definition().in(0, 0, 0, 4).out(1, 1, 0, 4)}; + RouterWithDefinitions router; + router.init(defs); + + writePdu(1, sizeof(uint32_t) - 1, router.rxWriters[0]); + ASSERT_FALSE(router.run()); + ASSERT_EQ(0, router.rxReaders[0].peek().size()); + ASSERT_EQ(0, router.txReaders[1].peek().size()); +} + +/** + * \desc + * Unknown PDUs are discarded and not routed to any channel + */ +TEST(RouterTest, unknown_pdus_are_not_routed) +{ + constexpr size_t MAX_NUM_CHANNELS = 4; + ::routing::Definition defs[] + = {::routing::Definition().in(0, 0x34, 0, 4).out(1, 0x01, 0, 4), + ::routing::Definition().in(1, 0x56, 0, 8).out(2, 0x02, 0, 8), + ::routing::Definition().in(2, 0x78, 0, 8).out(3, 0x03, 0, 8)}; + RouterWithDefinitions router; + router.init(defs); + + // write unknown PDU to each channel + for (size_t i = 0; i < MAX_NUM_CHANNELS; i++) + { + writePdu(0x7U + i, 16, router.rxWriters[i]); + } + + ASSERT_FALSE(router.run()); + + for (size_t i = 0; i < MAX_NUM_CHANNELS; i++) + { + ASSERT_EQ(0, router.rxReaders[i].peek().size()); + ASSERT_EQ(0, router.txReaders[i].peek().size()); + } +} + +/** + * \desc + * Routing PDUs to multiple channels works + */ +TEST(RouterTest, route_to_multiple_channels) +{ + constexpr size_t MAX_NUM_CHANNELS = 5; + ::routing::Definition defs[] = {::routing::Definition() + .in(0, 0x34, 0, 4) + .out(1, 0x01, 0, 4) + .out(2, 0x02, 0, 4) + .out(3, 0x03, 0, 4) + .out(4, 0x04, 0, 4)}; + RouterWithDefinitions router; + router.init(defs); + + writePdu(0, 12, router.rxWriters[0]); + ASSERT_TRUE(router.run()); + ASSERT_EQ(0, router.rxReaders[0].peek().size()); + for (size_t i = 1; i < MAX_NUM_CHANNELS; i++) + { + ASSERT_EQ(12, router.txReaders[i].peek().size()); + } +} + +/** + * \desc + * PDUs are routed in a round robin fashion. + */ +TEST(RouterTest, route_pdus_round_robin) +{ + constexpr size_t MAX_NUM_CHANNELS = 3; + ::routing::Definition defs[] + = {::routing::Definition().in(0, 0, 0, 4).out(1, 10, 0, 4), + ::routing::Definition().in(1, 1, 0, 4).out(2, 11, 0, 4), + ::routing::Definition().in(2, 2, 0, 4).out(0, 12, 0, 4)}; + RouterWithDefinitions router; + router.init(defs); + + writePdu(0, 12, router.rxWriters[0]); + writePdu(0, 12, router.rxWriters[0]); + writePdu(1, 12, router.rxWriters[1]); + writePdu(1, 12, router.rxWriters[1]); + writePdu(2, 12, router.rxWriters[2]); + writePdu(2, 12, router.rxWriters[2]); + + ASSERT_TRUE(router.run()); + ASSERT_EQ(12, router.txReaders[1].peek().size()); + + ASSERT_TRUE(router.run()); + ASSERT_EQ(12, router.txReaders[2].peek().size()); + + ASSERT_TRUE(router.run()); + ASSERT_EQ(12, router.txReaders[0].peek().size()); + + router.txReaders[0].release(); + router.txReaders[1].release(); + router.txReaders[2].release(); + + ASSERT_EQ(0, router.txReaders[0].peek().size()); + ASSERT_EQ(0, router.txReaders[1].peek().size()); + ASSERT_EQ(0, router.txReaders[2].peek().size()); + + ASSERT_TRUE(router.run()); + ASSERT_EQ(12, router.txReaders[1].peek().size()); + + ASSERT_TRUE(router.run()); + ASSERT_EQ(12, router.txReaders[2].peek().size()); + + ASSERT_TRUE(router.run()); + ASSERT_EQ(12, router.txReaders[0].peek().size()); +} + +/** + * \desc + * Routing all PDUs from defintion works + */ +TEST(RouterTest, all_pdus) +{ + constexpr size_t HEADER_SIZE = 8; + constexpr size_t MAX_NUM_CHANNELS = 5; + ::routing::Definition defs[] + = {::routing::Definition().in(0, 0x34, 3, 2).out(1, 0x44, 0, 2), + ::routing::Definition().in(0, 0x34, 0, 7).out(1, 0x45, 0, 7), + ::routing::Definition().in(0, 0x34, 0, 3).out(1, 0x43, 0, 3), + ::routing::Definition().in(0, 0x35, 0, 8).out(1, 0x46, 0, 8), + ::routing::Definition().in(0, 0x36, 0, 9).out(1, 0x47, 0, 9), + ::routing::Definition().in(1, 0x3, 8, 16).out(2, 0x5, 0, 16), + ::routing::Definition().in(1, 0x3, 0, 8).out(2, 0x4, 0, 8), + ::routing::Definition().in(2, 0x4, 0, 3).out(1, 0x34, 0, 3).out(4, 0x12, 0, 3), + ::routing::Definition().in(2, 0x5, 0, 1).out(0, 0x4, 0, 1), + ::routing::Definition().in(3, 0x33, 0, 3).out(2, 0x22, 0, 3), + ::routing::Definition().in(3, 0x34, 0, 3).out(4, 0x45, 0, 3), + ::routing::Definition().in(4, 0x44, 0, 4).out(3, 0x33, 0, 4)}; + + RouterWithDefinitions router; + router.init(defs); + + uint32_t internalPduId = 0; + ::etl::vector<::routing::DefinitionRef, 1024> definitions; + for (auto const& i : defs) + { + definitions.push_back({&i}); + } + ::routing::sort(definitions); + for (auto const definition : definitions) + { + auto def = *definition.d; + + // converting from messageId to internalPduId is the job of the RxAdapter + writePdu( + internalPduId, def.input.length + HEADER_SIZE, router.rxWriters[def.input.channelId]); + ASSERT_TRUE(router.run()); + + for (auto const& output : def.outputs) + { + // the length isn't modified by the routing core, this is the job of the txAdapter + auto& reader = router.txReaders[output.channelId]; + ASSERT_EQ(def.input.length + HEADER_SIZE, reader.peek().size()) + << internalPduId << " : " << (uint32_t)def.input.channelId << " : " + << (uint32_t)output.channelId; + EXPECT_EQ(output.messageId, reader.peek().take<::etl::be_uint32_t>()); + reader.release(); + } + + internalPduId++; + } +} + +/** + * \desc + * Routing PDUs from defintion works with a smaller number of channels + */ +TEST(RouterTest, all_pdus_smaller_number_of_channels) +{ + constexpr size_t HEADER_SIZE = 8; + constexpr uint8_t MAX_NUM_CHANNELS = 5; + constexpr uint8_t NUM_CHANNELS = MAX_NUM_CHANNELS - 1; + ::routing::Definition defs[] + = {::routing::Definition().in(0, 0x34, 3, 2).out(1, 0x44, 0, 2), + ::routing::Definition().in(0, 0x34, 0, 7).out(1, 0x45, 0, 7), + ::routing::Definition().in(0, 0x34, 0, 3).out(1, 0x43, 0, 3), + ::routing::Definition().in(0, 0x35, 0, 8).out(1, 0x46, 0, 8), + ::routing::Definition().in(0, 0x36, 0, 9).out(1, 0x47, 0, 9), + ::routing::Definition().in(1, 0x3, 8, 16).out(2, 0x5, 0, 16), + ::routing::Definition().in(1, 0x3, 0, 8).out(2, 0x4, 0, 8), + ::routing::Definition().in(2, 0x4, 0, 3).out(1, 0x34, 0, 3).out(4, 0x12, 0, 3), + ::routing::Definition().in(2, 0x5, 0, 1).out(0, 0x4, 0, 1), + ::routing::Definition().in(3, 0x33, 0, 3).out(2, 0x22, 0, 3), + ::routing::Definition().in(3, 0x34, 0, 3).out(4, 0x45, 0, 3), + ::routing::Definition().in(4, 0x44, 0, 4).out(3, 0x33, 0, 4)}; + + RouterWithDefinitions router; + + auto readers = ::etl::make_span(router.readers).first(NUM_CHANNELS); + auto writers = ::etl::make_span(router.writers).first(NUM_CHANNELS); + router.init(defs, readers, writers); + + uint32_t internalPduId = 0; + ::etl::vector<::routing::DefinitionRef, 1024> definitions; + for (auto const& i : defs) + { + definitions.push_back({&i}); + } + ::routing::sort(definitions); + for (auto const definition : definitions) + { + auto def = *definition.d; + + // converting from messageId to internalPduId is the job of the RxAdapter + writePdu( + internalPduId, def.input.length + HEADER_SIZE, router.rxWriters[def.input.channelId]); + + if (def.input.channelId >= NUM_CHANNELS) + { + ASSERT_FALSE(router.run()); + ASSERT_EQ( + def.input.length + HEADER_SIZE, + router.rxReaders[def.input.channelId].peek().size()); + router.rxReaders[def.input.channelId].release(); + continue; + } + + ASSERT_TRUE(router.run()); + for (auto const& output : def.outputs) + { + // the length isn't modified by the routing core, this is the job of the txAdapter + auto& reader = router.txReaders[output.channelId]; + if (output.channelId < NUM_CHANNELS) + { + ASSERT_EQ(def.input.length + HEADER_SIZE, reader.peek().size()) + << internalPduId << " : " << (uint32_t)def.input.channelId << " : " + << (uint32_t)output.channelId; + EXPECT_EQ(output.messageId, reader.peek().take<::etl::be_uint32_t>()); + reader.release(); + } + else + { + ASSERT_EQ(0U, reader.peek().size()); + } + } + + internalPduId++; + } +} + +/** + * \desc + * Routing PDUs from defintion works with a bigger number of channels + */ +TEST(RouterTest, all_pdus_bigger_number_of_channels) +{ + constexpr size_t HEADER_SIZE = 8; + constexpr uint8_t MAX_NUM_CHANNELS = 5; + constexpr uint8_t NUM_CHANNELS = MAX_NUM_CHANNELS + 1; + ::routing::Definition defs[] + = {::routing::Definition().in(0, 0x34, 3, 2).out(1, 0x44, 0, 2), + ::routing::Definition().in(0, 0x34, 0, 7).out(1, 0x45, 0, 7), + ::routing::Definition().in(0, 0x34, 0, 3).out(1, 0x43, 0, 3), + ::routing::Definition().in(0, 0x35, 0, 8).out(1, 0x46, 0, 8), + ::routing::Definition().in(0, 0x36, 0, 9).out(1, 0x47, 0, 9), + ::routing::Definition().in(1, 0x3, 8, 16).out(2, 0x5, 0, 16), + ::routing::Definition().in(1, 0x3, 0, 8).out(2, 0x4, 0, 8), + ::routing::Definition().in(2, 0x4, 0, 3).out(1, 0x34, 0, 3).out(4, 0x12, 0, 3), + ::routing::Definition().in(2, 0x5, 0, 1).out(0, 0x4, 0, 1), + ::routing::Definition().in(3, 0x33, 0, 3).out(2, 0x22, 0, 3), + ::routing::Definition().in(3, 0x34, 0, 3).out(4, 0x45, 0, 3), + ::routing::Definition().in(4, 0x44, 0, 4).out(3, 0x33, 0, 4).out(5, 0x55, 0, 4), + ::routing::Definition().in(4, 0x45, 0, 4).out(5, 0x56, 0, 4), + ::routing::Definition().in(5, 0x55, 0, 5).out(4, 0x44, 0, 5)}; + + RouterWithDefinitions router; + + decltype(router)::Queue rxQueue; + decltype(router)::Queue txQueue; + decltype(router)::Reader rxReader(rxQueue); + decltype(router)::Reader txReader(txQueue); + decltype(router)::Writer rxWriter(rxQueue); + decltype(router)::Writer txWriter(txQueue); + ::etl::array<::io::IReader*, NUM_CHANNELS> readers; + ::etl::array<::io::IWriter*, NUM_CHANNELS> writers; + + for (size_t i = 0; i < MAX_NUM_CHANNELS; ++i) + { + readers[i] = router.readers[i]; + writers[i] = router.writers[i]; + } + readers.back() = &rxReader; + writers.back() = &txWriter; + + router.init(defs, readers, writers); + + uint32_t internalPduId = 0; + ::etl::vector<::routing::DefinitionRef, 1024> definitions; + for (auto const& i : defs) + { + definitions.push_back({&i}); + } + ::routing::sort(definitions); + for (auto const definition : definitions) + { + auto def = *definition.d; + + auto& writer = def.input.channelId < MAX_NUM_CHANNELS + ? router.rxWriters[def.input.channelId] + : rxWriter; + // converting from messageId to internalPduId is the job of the RxAdapter + writePdu(internalPduId, def.input.length + HEADER_SIZE, writer); + + if (def.input.channelId >= MAX_NUM_CHANNELS) + { + ASSERT_FALSE(router.run()); + ASSERT_EQ(def.input.length + HEADER_SIZE, rxReader.peek().size()); + rxReader.release(); + continue; + } + + ASSERT_TRUE(router.run()); + for (auto const& output : def.outputs) + { + // the length isn't modified by the routing core, this is the job of the txAdapter + if (output.channelId < MAX_NUM_CHANNELS) + { + auto& reader = router.txReaders[output.channelId]; + ASSERT_EQ(def.input.length + HEADER_SIZE, reader.peek().size()) + << internalPduId << " : " << (uint32_t)def.input.channelId << " : " + << (uint32_t)output.channelId; + EXPECT_EQ(output.messageId, reader.peek().take<::etl::be_uint32_t>()); + reader.release(); + } + else + { + ASSERT_EQ(0U, txReader.peek().size()); + } + } + + internalPduId++; + } +} + +/** + * \desc + * Initializing router with empty blob doesn't crash. + */ +TEST(RouterTest, empty_blob) +{ + constexpr size_t MAX_NUM_CHANNELS = 2; + ::etl::span emptyBlob; + RouterWithDefinitions router; + + router.init(emptyBlob); + router.run(); +} + +/** + * \desc + * Reinitializing router does nothing. + */ +TEST(RouterTest, init_twice) +{ + constexpr size_t MAX_NUM_CHANNELS = 2; + ::routing::Definition defs[] = {::routing::Definition().in(0, 0x34, 0, 4).out(1, 0x43, 0, 4)}; + ::etl::span emptyBlob; + + RouterWithDefinitions router; + router.init(defs); + router.init(defs); + router.init(emptyBlob); + + writePdu(0U, 12, router.rxWriters[0]); + + ASSERT_TRUE(router.run()); +} + +/** + * \desc + * Router initializes and runs even when some readers are invalid. + */ +TEST(RouterTest, init_and_run_with_invalid_reader) +{ + constexpr size_t MAX_NUM_CHANNELS = 3; + ::routing::Definition defs[] = {::routing::Definition().in(1, 0x34, 0, 4).out(1, 0x43, 0, 4)}; + RouterWithDefinitions router; + + auto readers = router.readers; + readers[0] = nullptr; + readers[2] = nullptr; + router.init(defs, readers, router.writers); + + writePdu(0U, 12, router.rxWriters[0]); + writePdu(0U, 12, router.rxWriters[1]); + writePdu(0U, 12, router.rxWriters[2]); + + ASSERT_TRUE(router.run()); + ASSERT_EQ(12, router.txReaders[1].peek().size()); + EXPECT_EQ(0x43U, router.txReaders[1].peek().take<::etl::be_uint32_t>()); + router.txReaders[1].release(); +} + +/** + * \desc + * Router initializes and runs even when some writers are invalid. + */ +TEST(RouterTest, init_and_run_with_invalid_writer) +{ + constexpr size_t MAX_NUM_CHANNELS = 3; + ::routing::Definition defs[] = {::routing::Definition() + .in(1, 0x34, 0, 4) + .out(0, 0x40, 0, 4) + .out(1, 0x41, 0, 4) + .out(2, 0x42, 0, 4)}; + RouterWithDefinitions router; + + auto writers = router.writers; + writers[0] = nullptr; + writers[2] = nullptr; + router.init(defs, router.readers, writers); + + writePdu(0U, 12, router.rxWriters[1]); + + ASSERT_TRUE(router.run()); + ASSERT_EQ(0, router.txReaders[0].peek().size()); + ASSERT_EQ(12, router.txReaders[1].peek().size()); + EXPECT_EQ(0x41U, router.txReaders[1].peek().take<::etl::be_uint32_t>()); + router.txReaders[1].release(); + ASSERT_EQ(0, router.txReaders[2].peek().size()); +} + +/** + * \desc + * Router initializes and runs even when every reader is invalid. + */ +TEST(RouterTest, init_and_run_with_all_invalid_readers) +{ + constexpr size_t MAX_NUM_CHANNELS = 3; + ::routing::Definition defs[] = {::routing::Definition().in(1, 0x34, 0, 4).out(1, 0x43, 0, 4)}; + RouterWithDefinitions router; + + auto readers = router.readers; + for (size_t i = 0U; i < MAX_NUM_CHANNELS; ++i) + { + readers[i] = nullptr; + } + router.init(defs, readers, router.writers); + + for (size_t i = 0U; i < MAX_NUM_CHANNELS; ++i) + { + writePdu(0U, 12, router.rxWriters[i]); + } + + ASSERT_FALSE(router.run()); + for (size_t i = 0U; i < MAX_NUM_CHANNELS; ++i) + { + ASSERT_EQ(0, router.txReaders[i].peek().size()); + } +} + +/** + * \desc + * Router initializes and runs even when every writer is invalid. + */ +TEST(RouterTest, init_and_run_with_all_invalid_writers) +{ + constexpr size_t MAX_NUM_CHANNELS = 3; + ::routing::Definition defs[] = {::routing::Definition() + .in(1, 0x34, 0, 4) + .out(0, 0x40, 0, 4) + .out(1, 0x41, 0, 4) + .out(2, 0x42, 0, 4)}; + RouterWithDefinitions router; + + auto writers = router.writers; + for (size_t i = 0U; i < MAX_NUM_CHANNELS; ++i) + { + writers[i] = nullptr; + } + router.init(defs, router.readers, writers); + + writePdu(0U, 12, router.rxWriters[1]); + + ASSERT_TRUE(router.run()); + for (size_t i = 0U; i < MAX_NUM_CHANNELS; ++i) + { + ASSERT_EQ(0, router.txReaders[i].peek().size()); + } +} + +/** + * \desc + * Initialization fails if there are no destination offsets. + */ +TEST(RouterTest, empty_destination_offsets) +{ + constexpr size_t MAX_NUM_CHANNELS = 2; + + ::routing::Definition defs[] = {::routing::Definition().in(0, 0x34, 0, 4).out(1, 0x43, 0, 4)}; + RouterWithDefinitions routerWithDefs; + auto& table = routerWithDefs.pduRoutingTable; + auto& router = routerWithDefs.router; + + ::routing::load(table, defs, routerWithDefs.routingMem); + table.destinationOffsets = {}; + router.init(table, routerWithDefs.readers, routerWithDefs.writers); + + writePdu(0U, 12, routerWithDefs.rxWriters[0]); + + ASSERT_FALSE(router.run()); +} + +/** + * \desc + * Initialization fails if there are no output message IDs. + */ +TEST(RouterTest, empty_output_message_ids) +{ + constexpr size_t MAX_NUM_CHANNELS = 2; + + ::routing::Definition defs[] = {::routing::Definition().in(0, 0x34, 0, 4).out(1, 0x43, 0, 4)}; + RouterWithDefinitions routerWithDefs; + auto& table = routerWithDefs.pduRoutingTable; + auto& routingMem = routerWithDefs.routingMem; + auto& readers = routerWithDefs.readers; + auto& writers = routerWithDefs.writers; + auto& router = routerWithDefs.router; + auto& rxWriters = routerWithDefs.rxWriters; + + ::routing::load(table, defs, routingMem); + table.destinations = {}; + router.init(table, readers, writers); + + ::routing::load(table, defs, routingMem); + table.destinationOffsets = {}; + router.init(table, readers, writers); + + ::routing::load(table, defs, routingMem); + table.outputMessageIds = {}; + router.init(table, readers, writers); + + writePdu(0U, 12, rxWriters[0]); + + ASSERT_FALSE(router.run()); +} + +/** + * \desc + * Initialization fails if the number of destinations and message IDs do not match. + */ +TEST(RouterTest, different_number_of_destinations_and_message_ids) +{ + constexpr size_t MAX_NUM_CHANNELS = 3; + + ::routing::Definition defs[] + = {::routing::Definition().in(0, 0, 0, 4).out(1, 10, 0, 4), + ::routing::Definition().in(1, 1, 0, 4).out(2, 11, 0, 4), + ::routing::Definition().in(2, 2, 0, 4).out(0, 12, 0, 4)}; + RouterWithDefinitions routerWithDefs; + auto& table = routerWithDefs.pduRoutingTable; + auto& routingMem = routerWithDefs.routingMem; + auto& readers = routerWithDefs.readers; + auto& writers = routerWithDefs.writers; + auto& router = routerWithDefs.router; + auto& rxWriters = routerWithDefs.rxWriters; + + ::routing::load(table, defs, routingMem); + table.destinations = table.destinations.first(table.destinations.size() - 1); + router.init(table, readers, writers); + + ::routing::load(table, defs, routingMem); + table.outputMessageIds = table.outputMessageIds.first(table.outputMessageIds.size() - 1); + router.init(table, readers, writers); + + writePdu(0, 12, rxWriters[0]); + writePdu(1, 12, rxWriters[1]); + writePdu(2, 12, rxWriters[2]); + + ASSERT_FALSE(router.run()); +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/RxAdapterTableTest.cpp b/libs/bsw/routing/test/src/routing/RxAdapterTableTest.cpp new file mode 100644 index 00000000000..41003ca6d4d --- /dev/null +++ b/libs/bsw/routing/test/src/routing/RxAdapterTableTest.cpp @@ -0,0 +1,159 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/RxAdapterTable.h" + +#include "blob/configuration.h" +#include "routing/constants.h" +#include "routing/util.h" + +#include + +#include + +#include + +namespace +{ +using namespace ::testing; + +/** + * \desc + * Loading the RX adapter configs from a test blob gives the correct values. + */ +TEST(RxAdapterTableTest, rx_adapter_config) +{ + uint8_t const channelIds[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + uint32_t const firstIds[] = {0, 7, 10, 10, 17, 18, 18, 20, 20, 20, 21, 22}; + + std::vector> const messageIds + = {{1, 2, 3, 4, 257}, + {2561, 4242, 3435969450}, + {}, + {2, 17, 18, 19, 20, 21, 257}, + {20480}, + {}, + {444, 445}, + {}, + {}, + {777}, + {888}, + {}}; + + std::vector> const messageLengths + = {{}, {}, {}, {8, 8, 8, 8, 8, 8, 8}, {8}, {}, {8, 8}, {}, {}, {8}, {8}, {}}; + + std::vector> const pduLengthsOffsets + = {{0, 1, 4, 5, 6, 7}, + {0, 1, 2, 3}, + {0}, + {0, 1, 2, 3, 4, 5, 6, 7}, + {0, 1}, + {0}, + {0, 1, 2}, + {0}, + {0}, + {0, 1}, + {0, 1}, + {0}}; + + std::vector> const pduLengths + = {{8, 8, 4, 4, 8, 8, 8}, + {64, 64, 64}, + {}, + {8, 8, 8, 8, 8, 8, 8}, + {8}, + {}, + {8, 8}, + {}, + {}, + {8}, + {8}, + {}}; + + std::vector> const pduOffsets + = {{0, 0, 0, 4, 0, 0, 0}, + {0, 0, 0}, + {}, + {0, 0, 0, 0, 0, 0, 0}, + {0}, + {}, + {0, 0}, + {}, + {}, + {0}, + {0}, + {}}; + + ::blob::Blob const blob(::blob::CONFIGURATION_BLOB); + + for (size_t i = 0; i < ::routing::NUM_CHANNELS; ++i) + { + auto const adapterConfig = ::routing::config( + ::blob::CONFIGURATION_BLOB, ::blob::Config::Type::RX_ADAPTER, channelIds[i]); + EXPECT_EQ(::blob::Config::Type::RX_ADAPTER, adapterConfig.type); + + ::routing::RxAdapterTable table; + EXPECT_TRUE(routing::load(adapterConfig.data, table)); + + EXPECT_EQ(firstIds[i], table.firstId); + + EXPECT_EQ(messageIds[i].size(), table.messageIds.size()); + EXPECT_THAT(table.messageIds, ElementsAreArray(messageIds[i])); + + EXPECT_EQ(messageLengths[i].size(), table.messageLengths.size()); + EXPECT_THAT(table.messageLengths, ElementsAreArray(messageLengths[i])); + + EXPECT_EQ(pduLengthsOffsets[i].size(), table.pduLengthsOffsets.size()); + EXPECT_THAT(table.pduLengthsOffsets, ElementsAreArray(pduLengthsOffsets[i])); + + EXPECT_EQ(pduLengths[i].size(), table.pduLengths.size()); + EXPECT_THAT(table.pduLengths, ElementsAreArray(pduLengths[i])); + + EXPECT_EQ(pduOffsets[i].size(), table.pduOffsets.size()); + EXPECT_THAT(table.pduOffsets, ElementsAreArray(pduOffsets[i])); + } +} + +/** + * \desc + * Loading from a corrupted config returns a default-constructed table. + */ +TEST(RxAdapterTableTest, corrupted_rx_adapter_config) +{ + uint8_t corruptedRxAdapterConfigData[] + = {0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x5D, 0xD1, 0x9A}; + + ::etl::span rxAdapterConfigData = ::etl::make_span(corruptedRxAdapterConfigData); + rxAdapterConfigData.take<::etl::be_uint32_t>() = 3U; // config type + + ::routing::RxAdapterTable table; + + EXPECT_FALSE(routing::load(corruptedRxAdapterConfigData, table)); +} + +/** + * \desc + * Loading config from an empty blob doesn't crash. + */ +TEST(RxAdapterTableTest, empty_blob) +{ + ::etl::span emptyBlob; + auto const rxAdapterConfig = ::routing::config(emptyBlob, ::blob::Config::Type::RX_ADAPTER, 42); + EXPECT_EQ(0, rxAdapterConfig.data.size()); + + ::routing::RxAdapterTable table; + EXPECT_FALSE(routing::load(rxAdapterConfig.data, table)); +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/RxAdapterTest.cpp b/libs/bsw/routing/test/src/routing/RxAdapterTest.cpp new file mode 100644 index 00000000000..c3b93385cf3 --- /dev/null +++ b/libs/bsw/routing/test/src/routing/RxAdapterTest.cpp @@ -0,0 +1,589 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/RxAdapter.h" + +#include "blob/configuration.h" +#include "routing/ErrorHandler.h" +#include "routing/definition.h" +#include "routing/util.h" + +#include +#include +#include +#include + +#include + +#include + +namespace +{ +using namespace ::testing; + +template +struct RxAdapterTest +{ + using Queue = ::io::MemoryQueue; + using Reader = ::io::MemoryQueueReader; + using Writer = ::io::MemoryQueueWriter; + + RxAdapterTest(::etl::span<::routing::Definition> defs) + : rxQueue() + , rxReader(rxQueue) + , rxWriter(rxQueue) + , invalidHeaderSizeCounter(0) + , unknownMessageIdCounter(0) + , invalidParsedLengthCounter(0) + , invalidPayloadLengthCounter(0) + , errorHandler( + ::routing::ErrorHandler::Function::create< + RxAdapterTest, + &RxAdapterTest::handleError>(*this), + 0U) + { + routing::load(rxAdapterTable, defs, 0, rxAdapterTableMem); + (void)rxAdapter.emplace(rxReader, buffer, rxAdapterTable, errorHandler); + } + + void handleError(::routing::ErrorHandler::StatusCode const statusCode, uint8_t, uint32_t) + { + switch (statusCode) + { + case ::routing::ErrorHandler::StatusCode::INVALID_MESSAGE_HEADER_SIZE: + ++invalidHeaderSizeCounter; + break; + case ::routing::ErrorHandler::StatusCode::UNKNOWN_MESSAGE_ID: + ++unknownMessageIdCounter; + break; + case ::routing::ErrorHandler::StatusCode::INVALID_PARSED_LENGTH: + ++invalidParsedLengthCounter; + break; + case ::routing::ErrorHandler::StatusCode::INVALID_PAYLOAD_LENGTH: + ++invalidPayloadLengthCounter; + break; + default: break; + } + } + + Queue rxQueue; + Reader rxReader; + Writer rxWriter; + + ::etl::array buffer; + + ::routing::RxAdapterTableMem rxAdapterTableMem; + ::routing::RxAdapterTable rxAdapterTable; + + ::etl::optional<::routing::RxAdapter> rxAdapter; + + uint32_t invalidHeaderSizeCounter; + uint32_t unknownMessageIdCounter; + uint32_t invalidParsedLengthCounter; + uint32_t invalidPayloadLengthCounter; + + ::routing::ErrorHandler errorHandler; +}; + +::etl::span allocatePdu(uint32_t const id, size_t const size, ::io::IWriter& writer) +{ + auto pdu = writer.allocate(size); + if (pdu.size() >= ::routing::RxAdapter::MESSAGE_HEADER_SIZE) + { + auto header = pdu.first(::routing::RxAdapter::MESSAGE_HEADER_SIZE); + header.take<::etl::be_uint32_t>() = id; + auto& length = header.take<::etl::be_uint32_t>(); + length = size - ::routing::RxAdapter::MESSAGE_HEADER_SIZE; + } + return pdu; +} + +void writePdu(uint32_t const id, size_t const size, ::io::IWriter& writer) +{ + auto pdu = allocatePdu(id, size, writer); + if (!pdu.empty()) + { + writer.commit(); + } +} + +/** + * \desc + * The maxSize function returns the size of the buffer provided. + */ +TEST(RxAdapterTest, max_size) +{ + RxAdapterTest<> rxAdapterTest({}); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + EXPECT_EQ(rxAdapterTest.buffer.size(), rxAdapter.maxSize()); +} + +/** + * \desc + * A single PDU is extracted out of a message. + */ +TEST(RxAdapterTest, extract_pdu) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint32_t OFFSET = 0; + constexpr uint32_t PAYLOAD_LENGTH = 8; + constexpr uint32_t PDU_ID = 0; + uint8_t expectedPayload[PAYLOAD_LENGTH] = {0, 1, 2, 3, 4, 5, 6, 7}; + + ::routing::Definition defs[] + = {::routing::Definition().in(0, MESSAGE_ID, OFFSET, PAYLOAD_LENGTH)}; + RxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + auto message = allocatePdu(MESSAGE_ID, size, rxWriter); + ::etl::copy( + ::etl::make_span(expectedPayload), + message.subspan(::routing::RxAdapter::MESSAGE_HEADER_SIZE)); + rxWriter.commit(); + + auto const pdu = rxAdapter.peek(); + EXPECT_EQ(size, pdu.size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(size, rxAdapter.byteCounter()); + EXPECT_EQ(0, rxAdapter.discardedPduCounter()); + EXPECT_EQ(0, rxAdapter.discardedByteCounter()); + + auto res = pdu; + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(PDU_ID, id); + + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(PAYLOAD_LENGTH, l); + EXPECT_THAT(res, ElementsAreArray(expectedPayload)); + EXPECT_THAT(rxAdapter.peek(), ElementsAreArray(pdu)); + + rxAdapter.release(); + EXPECT_EQ(0, rxAdapter.peek().size()); +} + +/** + * \desc + * Multiple PDUs are extracted out of a single message. + */ +TEST(RxAdapterTest, extract_multiple_pdus) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint8_t PAYLOAD_LENGTH = 8; + uint8_t expectedPayload[PAYLOAD_LENGTH] = {0, 1, 2, 3, 4, 5, 6, 7}; + + constexpr size_t NUMBER_OF_PDUS = 3; + uint32_t offsets[NUMBER_OF_PDUS] = {0, 0, 4}; + uint32_t payloadLengths[NUMBER_OF_PDUS] = {8, 4, 4}; + + ::routing::Definition defs[] + = {::routing::Definition().in(0, MESSAGE_ID, offsets[0], payloadLengths[0]), + ::routing::Definition().in(0, MESSAGE_ID, offsets[1], payloadLengths[1]), + ::routing::Definition().in(0, MESSAGE_ID, offsets[2], payloadLengths[2])}; + RxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + auto message = allocatePdu(MESSAGE_ID, size, rxWriter); + ::etl::copy( + ::etl::make_span(expectedPayload), + message.subspan(::routing::RxAdapter::MESSAGE_HEADER_SIZE)); + rxWriter.commit(); + + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + auto const pdu = rxAdapter.peek(); + EXPECT_EQ(::routing::RxAdapter::MESSAGE_HEADER_SIZE + payloadLengths[i], pdu.size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(size, rxAdapter.byteCounter()); + EXPECT_EQ(0, rxAdapter.discardedPduCounter()); + EXPECT_EQ(0, rxAdapter.discardedByteCounter()); + + auto res = pdu; + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(i, id); + + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(payloadLengths[i], l); + EXPECT_THAT( + res, + ElementsAreArray( + ::etl::make_span(expectedPayload).subspan(offsets[i], payloadLengths[i]))); + EXPECT_THAT(rxAdapter.peek(), ElementsAreArray(pdu)); + + rxAdapter.release(); + } + + EXPECT_EQ(0, rxAdapter.peek().size()); +} + +/** + * \desc + * Instead of extracting two PDUs, only one is extracted out of a shorter message. + */ +TEST(RxAdapterTest, extract_pdu_from_shorter_message) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint32_t OFFSET = 0; + constexpr uint32_t PAYLOAD_LENGTH = 4; + constexpr uint32_t PDU_ID = 0; + uint8_t expectedPayload[PAYLOAD_LENGTH] = {0, 1, 2, 3}; + + ::routing::Definition defs[] + = {::routing::Definition().in(0, MESSAGE_ID, OFFSET, PAYLOAD_LENGTH), + ::routing::Definition().in(0, MESSAGE_ID, OFFSET, PAYLOAD_LENGTH + 1)}; + + RxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + auto message = allocatePdu(MESSAGE_ID, size, rxWriter); + ::etl::copy( + ::etl::make_span(expectedPayload), + message.subspan(::routing::RxAdapter::MESSAGE_HEADER_SIZE)); + rxWriter.commit(); + + auto const pdu = rxAdapter.peek(); + EXPECT_EQ(::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH, pdu.size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(size, rxAdapter.byteCounter()); + EXPECT_EQ(0, rxAdapter.discardedPduCounter()); + EXPECT_EQ(0, rxAdapter.discardedByteCounter()); + + auto res = pdu; + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(PDU_ID, id); + + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(PAYLOAD_LENGTH, l); + EXPECT_THAT(res, ElementsAreArray(expectedPayload)); + EXPECT_THAT(rxAdapter.peek(), ElementsAreArray(pdu)); + + rxAdapter.release(); + EXPECT_EQ(0, rxAdapter.peek().size()); +} + +/** + * \desc + * A PDU is extracted out of a longer message. + */ +TEST(RxAdapterTest, extract_pdu_from_longer_message) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint32_t OFFSET = 0; + constexpr uint32_t PAYLOAD_LENGTH = 8; + constexpr uint32_t PDU_ID = 0; + uint8_t expectedPayload[PAYLOAD_LENGTH] = {0, 1, 2, 3, 4, 5, 6, 7}; + + ::routing::Definition defs[] + = {::routing::Definition().in(0, MESSAGE_ID, OFFSET, PAYLOAD_LENGTH)}; + RxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH + 1; + auto message = allocatePdu(MESSAGE_ID, size, rxWriter); + ::etl::copy( + ::etl::make_span(expectedPayload), + message.subspan(::routing::RxAdapter::MESSAGE_HEADER_SIZE)); + rxWriter.commit(); + + auto const pdu = rxAdapter.peek(); + EXPECT_EQ(::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH, pdu.size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(size, rxAdapter.byteCounter()); + EXPECT_EQ(0, rxAdapter.discardedPduCounter()); + EXPECT_EQ(0, rxAdapter.discardedByteCounter()); + + auto res = pdu; + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(PDU_ID, id); + + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(PAYLOAD_LENGTH, l); + EXPECT_THAT(res, ElementsAreArray(expectedPayload)); + EXPECT_THAT(rxAdapter.peek(), ElementsAreArray(pdu)); + + rxAdapter.release(); + EXPECT_EQ(0, rxAdapter.peek().size()); +} + +/** + * \desc + * Multiple PDUs are extracted out of some messages. + */ +TEST(RxAdapterTest, extract_pdus_from_some_messages) +{ + constexpr size_t NUMBER_OF_PDUS = 6; + constexpr size_t MAX_PAYLOAD_LENGTH = 8; + + uint32_t messageIds[NUMBER_OF_PDUS] = {1, 2, 2, 3, 3, 3}; + uint32_t offsets[NUMBER_OF_PDUS] = {0, 0, 4, 0, 4, 0}; + uint32_t payloadLengths[NUMBER_OF_PDUS] = {8, 8, 4, 8, 4, 4}; + uint8_t expectedPayload[MAX_PAYLOAD_LENGTH] = {0, 1, 2, 3, 4, 5, 6, 7}; + uint32_t expectedPduCounter = 0U, expectedByteCounter = 0U; + + ::etl::vector<::routing::Definition, NUMBER_OF_PDUS> defs; + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + defs.emplace_back( + ::routing::Definition().in(0, messageIds[i], offsets[i], payloadLengths[i])); + } + + RxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + MAX_PAYLOAD_LENGTH; + uint32_t messageId = 0; + for (size_t i = 0; i < NUMBER_OF_PDUS; ++i) + { + if (messageId != messageIds[i]) + { + messageId = messageIds[i]; + auto message = allocatePdu(messageId, size, rxWriter); + ::etl::copy( + ::etl::make_span(expectedPayload), + message.subspan(::routing::RxAdapter::MESSAGE_HEADER_SIZE)); + rxWriter.commit(); + + ++expectedPduCounter; + expectedByteCounter += size; + } + + auto const payloadLength = payloadLengths[i]; + auto const offset = offsets[i]; + + auto const pdu = rxAdapter.peek(); + EXPECT_EQ(pdu.size(), ::routing::RxAdapter::MESSAGE_HEADER_SIZE + payloadLength); + EXPECT_EQ(expectedPduCounter, rxAdapter.pduCounter()); + EXPECT_EQ(expectedByteCounter, rxAdapter.byteCounter()); + EXPECT_EQ(rxAdapter.discardedPduCounter(), 0); + EXPECT_EQ(rxAdapter.discardedByteCounter(), 0); + + auto res = pdu; + uint32_t id = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(i, id); + + uint32_t l = res.take<::etl::be_uint32_t>(); + EXPECT_EQ(payloadLength, l); + + EXPECT_THAT( + res, + ElementsAreArray(::etl::make_span(expectedPayload).subspan(offset, payloadLength))); + EXPECT_THAT(rxAdapter.peek(), ElementsAreArray(pdu)); + + rxAdapter.release(); + } + + EXPECT_EQ(0, rxAdapter.peek().size()); +} + +/** + * \desc + * PDU is dropped if message header size is invalid. + */ +TEST(RxAdapterTest, discard_pdu_invalid_message_header_size) +{ + constexpr uint32_t MESSAGE_ID = 1; + + ::routing::Definition defs[] = {::routing::Definition().in(0, MESSAGE_ID, 0, 8)}; + RxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE - 1; + writePdu(MESSAGE_ID, size, rxWriter); + + EXPECT_EQ(0, rxAdapter.peek().size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(size, rxAdapter.byteCounter()); + EXPECT_EQ(1, rxAdapter.discardedPduCounter()); + EXPECT_EQ(size, rxAdapter.discardedByteCounter()); + EXPECT_EQ(1, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(1, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(0, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(0, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(0, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(0, rxAdapterTest.invalidPayloadLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidPayloadLengthPdus()); +} + +/** + * \desc + * PDU is dropped if message ID is unknown. + */ +TEST(RxAdapterTest, discard_pdu_unknown_message_id) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint32_t PAYLOAD_LENGTH = 8; + + ::routing::Definition defs[] = {::routing::Definition().in(0, MESSAGE_ID, 0, PAYLOAD_LENGTH)}; + RxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + writePdu(MESSAGE_ID + 1, size, rxWriter); + + EXPECT_EQ(0, rxAdapter.peek().size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(size, rxAdapter.byteCounter()); + EXPECT_EQ(1, rxAdapter.discardedPduCounter()); + EXPECT_EQ(size, rxAdapter.discardedByteCounter()); + EXPECT_EQ(0, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(0, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(1, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(1, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(0, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(0, rxAdapterTest.invalidPayloadLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidPayloadLengthPdus()); +} + +/** + * \desc + * PDU is dropped if parsed length is invalid. + */ +TEST(RxAdapterTest, discard_pdu_invalid_parsed_length) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint32_t PAYLOAD_LENGTH = 8; + + ::routing::Definition defs[] = {::routing::Definition().in(0, MESSAGE_ID, 0, PAYLOAD_LENGTH)}; + RxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + auto pdu = rxWriter.allocate(size); + pdu.take<::etl::be_uint32_t>() = MESSAGE_ID; + pdu.take<::etl::be_uint32_t>() = PAYLOAD_LENGTH + 1; + rxWriter.commit(); + + EXPECT_EQ(0, rxAdapter.peek().size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(size, rxAdapter.byteCounter()); + EXPECT_EQ(1, rxAdapter.discardedPduCounter()); + EXPECT_EQ(size, rxAdapter.discardedByteCounter()); + EXPECT_EQ(0, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(0, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(0, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(0, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(1, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(1, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(0, rxAdapterTest.invalidPayloadLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidPayloadLengthPdus()); +} + +/** + * \desc + * PDU is discarded if payload length is invalid. If at least one PDU is extracted out of a message, + * it is not discarded. + */ +TEST(RxAdapterTest, discard_pdu_invalid_payload_length) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint8_t PAYLOAD_LENGTH = 4; + + ::routing::Definition defs[] + = {::routing::Definition().in(0, MESSAGE_ID, 0, PAYLOAD_LENGTH + 1), + ::routing::Definition().in(0, MESSAGE_ID, 0, PAYLOAD_LENGTH)}; + RxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + + auto const validSize = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + auto const invalidSize = validSize - 1; + + writePdu(MESSAGE_ID, validSize, rxWriter); + + EXPECT_EQ(validSize, rxAdapter.peek().size()); + EXPECT_EQ(1, rxAdapter.pduCounter()); + EXPECT_EQ(validSize, rxAdapter.byteCounter()); + EXPECT_EQ(0, rxAdapter.discardedPduCounter()); + EXPECT_EQ(0, rxAdapter.discardedByteCounter()); + EXPECT_EQ(0, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(0, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(0, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(0, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(0, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(0, rxAdapterTest.invalidPayloadLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidPayloadLengthPdus()); + + rxAdapter.release(); + writePdu(MESSAGE_ID, invalidSize, rxWriter); + + EXPECT_EQ(0, rxAdapter.peek().size()); + EXPECT_EQ(2, rxAdapter.pduCounter()); + EXPECT_EQ(validSize + invalidSize, rxAdapter.byteCounter()); + EXPECT_EQ(1, rxAdapter.discardedPduCounter()); + EXPECT_EQ(invalidSize, rxAdapter.discardedByteCounter()); + EXPECT_EQ(0, rxAdapterTest.invalidHeaderSizeCounter); + EXPECT_EQ(0, rxAdapter.invalidHeaderSizePdus()); + EXPECT_EQ(0, rxAdapterTest.unknownMessageIdCounter); + EXPECT_EQ(0, rxAdapter.unknownMessagePdus()); + EXPECT_EQ(0, rxAdapterTest.invalidParsedLengthCounter); + EXPECT_EQ(0, rxAdapter.invalidParsedPayloadLengthPdus()); + EXPECT_EQ(1, rxAdapterTest.invalidPayloadLengthCounter); + EXPECT_EQ(1, rxAdapter.invalidPayloadLengthPdus()); +} + +/** + * \desc + * Invalid PDUs are discarded before extracting one from a message. + */ +TEST(RxAdapterTest, discard_invalid_pdus) +{ + constexpr uint32_t MESSAGE_ID = 1; + constexpr uint8_t PAYLOAD_LENGTH = 8; + + ::routing::Definition defs[] = {::routing::Definition().in(0, MESSAGE_ID, 0, PAYLOAD_LENGTH)}; + RxAdapterTest<> rxAdapterTest(defs); + auto& rxAdapter = *rxAdapterTest.rxAdapter; + auto& rxWriter = rxAdapterTest.rxWriter; + uint32_t expectedPduCounter = 0U, expectedByteCounter = 0U, expectedDiscardedPduCounter = 0U, + expectedDiscardedByteCounter = 0U; + + constexpr size_t size = ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH; + // invalid header size + writePdu(MESSAGE_ID, ::routing::RxAdapter::MESSAGE_HEADER_SIZE - 1, rxWriter); + // unknown message ID + writePdu(MESSAGE_ID + 1, size, rxWriter); + // invalid parsed length + auto message = rxWriter.allocate(size); + message.take<::etl::be_uint32_t>() = MESSAGE_ID; + message.take<::etl::be_uint32_t>() = PAYLOAD_LENGTH + 1; + rxWriter.commit(); + // invalid payload length + writePdu(MESSAGE_ID, size - 1, rxWriter); + + writePdu(MESSAGE_ID, ::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH, rxWriter); + + auto pdu = rxAdapter.peek(); + + expectedPduCounter += 5; + expectedByteCounter += 70; + expectedDiscardedPduCounter += 4; + expectedDiscardedByteCounter += 54; + + EXPECT_EQ(::routing::RxAdapter::MESSAGE_HEADER_SIZE + PAYLOAD_LENGTH, pdu.size()); + EXPECT_EQ(expectedPduCounter, rxAdapter.pduCounter()); + EXPECT_EQ(expectedByteCounter, rxAdapter.byteCounter()); + EXPECT_EQ(expectedDiscardedPduCounter, rxAdapter.discardedPduCounter()); + EXPECT_EQ(expectedDiscardedByteCounter, rxAdapter.discardedByteCounter()); +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/TxAdapterTableTest.cpp b/libs/bsw/routing/test/src/routing/TxAdapterTableTest.cpp new file mode 100644 index 00000000000..7e27262d1e7 --- /dev/null +++ b/libs/bsw/routing/test/src/routing/TxAdapterTableTest.cpp @@ -0,0 +1,131 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/TxAdapterTable.h" + +#include "blob/configuration.h" +#include "routing/constants.h" +#include "routing/util.h" + +#include + +#include + +#include + +namespace +{ +using namespace ::testing; + +/** + * \desc + * Loading the TX adapter configs from a test blob gives the correct values. + */ +TEST(TxAdapterTableTest, tx_adapter_config) +{ + uint8_t const channelIds[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + std::vector> const messageIds + = {{257, 512, 1, 2, 5}, + {768}, + {}, + {256, 257, 258, 259, 2, 4242, 4242, 4242, 22, 111}, + {16384, 4096, 4098, 4099, 4100, 222}, + {333}, + {}, + {555}, + {666}, + {}, + {}, + {999}}; + + std::vector> const messageLengths + = {{8, 8, 8, 8, 8}, + {8}, + {}, + {8, 8, 4, 8, 8, 64, 64, 64, 8, 8}, + {8, 8, 8, 8, 8, 8}, + {8}, + {}, + {8}, + {8}, + {}, + {}, + {8}}; + + std::vector> const pduOffsets + = {{0, 0, 0, 0, 0}, + {0}, + {}, + {0, 0, 0, 4, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + {0}, + {}, + {0}, + {0}, + {}, + {}, + {0}}; + + ::blob::Blob const blob(::blob::CONFIGURATION_BLOB); + + for (size_t i = 0; i < ::routing::NUM_CHANNELS; ++i) + { + auto const adapterConfig = ::routing::config( + ::blob::CONFIGURATION_BLOB, ::blob::Config::Type::TX_ADAPTER, channelIds[i]); + EXPECT_EQ(::blob::Config::Type::TX_ADAPTER, adapterConfig.type); + + ::routing::TxAdapterTable table; + EXPECT_TRUE(routing::load(adapterConfig.data, table)); + + EXPECT_EQ(messageIds[i].size(), table.messageIds.size()); + EXPECT_THAT(table.messageIds, ElementsAreArray(messageIds[i])); + + EXPECT_EQ(messageLengths[i].size(), table.messageLengths.size()); + EXPECT_THAT(table.messageLengths, ElementsAreArray(messageLengths[i])); + + EXPECT_EQ(pduOffsets[i].size(), table.pduOffsets.size()); + EXPECT_THAT(table.pduOffsets, ElementsAreArray(pduOffsets[i])); + } +} + +/** + * \desc + * Loading from a corrupted config returns a default-constructed table. + */ +TEST(TxAdapterTableTest, corrupted_tx_adapter_config) +{ + uint8_t corruptedTxAdapterConfigData[] + = {0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA4, 0x14, 0x59, 0xED}; + + ::etl::span txAdapterConfigData = ::etl::make_span(corruptedTxAdapterConfigData); + txAdapterConfigData.take<::etl::be_uint32_t>() = 3U; // config type + + ::routing::TxAdapterTable table; + + EXPECT_FALSE(routing::load(corruptedTxAdapterConfigData, table)); +} + +/** + * \desc + * Loading config from an empty blob doesn't crash. + */ +TEST(TxAdapterTableTest, empty_blob) +{ + ::etl::span emptyBlob; + auto const txAdapterConfig = ::routing::config(emptyBlob, ::blob::Config::Type::TX_ADAPTER, 42); + EXPECT_EQ(0, txAdapterConfig.data.size()); + + ::routing::TxAdapterTable table; + EXPECT_FALSE(routing::load(txAdapterConfig.data, table)); +} + +} // namespace diff --git a/libs/bsw/routing/test/src/routing/definition.cpp b/libs/bsw/routing/test/src/routing/definition.cpp new file mode 100644 index 00000000000..1550f54d8a0 --- /dev/null +++ b/libs/bsw/routing/test/src/routing/definition.cpp @@ -0,0 +1,149 @@ +/******************************************************************************** + * Copyright (c) 2026 BMW AG + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "routing/definition.h" + +#include "routing/PduRoutingTable.h" +#include "routing/pduRouting.h" + +#include +#include + +namespace routing +{ +void sort(::etl::span const definitions) +{ + ::etl::sort( + definitions.begin(), + definitions.end(), + [](DefinitionRef const& a, DefinitionRef const& b) + { + if (a.d->input.channelId != b.d->input.channelId) + { + return (a.d->input.channelId < b.d->input.channelId); + } + return (a.d->input.messageId < b.d->input.messageId); + }); +} + +void load( + RxAdapterTable& table, + ::etl::span input, + uint8_t const channelId, + RxAdapterTableMem& mem) +{ + ::etl::vector definition; + for (auto const& i : input) + { + definition.push_back({&i}); + } + sort(definition); + + auto const channelBegin = ::etl::find_if( + definition.begin(), + definition.end(), + [channelId](DefinitionRef const e) { return e.d->input.channelId == channelId; }); + auto const channelEnd = ::etl::find_if( + channelBegin, + definition.end(), + [channelId](DefinitionRef const e) { return e.d->input.channelId != channelId; }); + + uint32_t currentPduOffset = 0; + uint32_t currentMsgId = 0; + for (auto e = channelBegin; e < channelEnd; e++) + { + if (currentMsgId != e->d->input.messageId) + { + currentMsgId = e->d->input.messageId; + mem.messageIds.emplace_back(::etl::be_uint32_t(currentMsgId)); + auto const currentMsgLength = e->d->input.messageLength; + if (currentMsgLength > 0) + { + mem.messageLengths.emplace_back(::etl::be_uint32_t(currentMsgLength)); + } + mem.pduLengthsOffsets.push_back(::etl::be_uint32_t(currentPduOffset)); + } + mem.pduLengths.push_back(::etl::be_uint32_t(e->d->input.length)); + mem.pduOffsets.push_back(::etl::be_uint32_t(e->d->input.offset)); + ++currentPduOffset; + } + mem.pduLengthsOffsets.push_back(::etl::be_uint32_t(currentPduOffset)); + + table.firstId = static_cast(::etl::distance(definition.begin(), channelBegin)); + table.messageIds = mem.messageIds; + table.messageLengths = {}; + if (mem.messageIds.size() == mem.messageLengths.size()) + { + table.messageLengths = mem.messageLengths; + } + table.pduLengthsOffsets = mem.pduLengthsOffsets; + table.pduLengths = mem.pduLengths; + table.pduOffsets = mem.pduOffsets; +} + +void load( + TxAdapterTable& table, + ::etl::span input, + uint8_t const channelId, + TxAdapterTableMem& mem) +{ + ::etl::vector definition; + for (auto const& i : input) + { + definition.push_back({&i}); + } + sort(definition); + + for (auto const& e : definition) + { + for (auto const& o : e.d->outputs) + { + if (o.channelId == channelId) + { + mem.messageIds.push_back(::etl::be_uint32_t(o.messageId)); + mem.messageLengths.push_back(::etl::be_uint32_t(o.messageLength)); + mem.offsets.push_back(::etl::be_uint32_t(o.offset)); + } + } + } + + table.messageIds = mem.messageIds; + table.messageLengths = mem.messageLengths; + table.pduOffsets = mem.offsets; +} + +void load(PduRoutingTable& table, ::etl::span input, RoutingTableMem& mem) +{ + ::etl::vector definition; + for (auto const& i : input) + { + definition.push_back({&i}); + } + sort(definition); + + uint32_t currentRoutingDestinationIndex = 0; + mem.destinationOffsets.emplace_back(::etl::be_uint32_t(currentRoutingDestinationIndex)); + for (auto const e : definition) + { + for (auto const& d : e.d->outputs) + { + mem.destinations.emplace_back(d.channelId); + mem.outputMessageIds.emplace_back(d.messageId); + ++currentRoutingDestinationIndex; + } + mem.destinationOffsets.emplace_back(::etl::be_uint32_t(currentRoutingDestinationIndex)); + } + + table.destinationOffsets = mem.destinationOffsets; + table.destinations = mem.destinations; + table.outputMessageIds = mem.outputMessageIds; +} + +} // namespace routing diff --git a/tools/blob/NOTICE.md b/tools/blob/NOTICE.md new file mode 100644 index 00000000000..b5856aae063 --- /dev/null +++ b/tools/blob/NOTICE.md @@ -0,0 +1,20 @@ +# GWTB License Information + +## Copyright + +All content is the property of the respective authors or their employers. +For more information regarding authorship of content, please consult the +individual source files and the source source code repository logs. + +## Declared Project Licenses + +The code and the accompanying materials in the following files and directories +are made available under the terms of the Apache v2 License. +See [LICENSE](LICENSE). + +- `__init__.py` +- `__main__.py` +- `utils.py` +- `routing/*` + +SPDX-License-Identifier: Apache-2.0 diff --git a/tools/blob/__init__.py b/tools/blob/__init__.py new file mode 100644 index 00000000000..26ffb02ee02 --- /dev/null +++ b/tools/blob/__init__.py @@ -0,0 +1,85 @@ +# ******************************************************************************* +# Copyright (c) 2026 BMW AG +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +__all__ = ["utils", "routing"] + +import string +from abc import ABC, abstractmethod +from enum import IntEnum +from dataclasses import dataclass +from itertools import chain +from typing import List, Union + + +class ExitCode(IntEnum): + SUCCESS = 0 + FAILURE = -1 + + +class IConfig(ABC): + @staticmethod + @abstractmethod + def new(objects): + pass + + +class ConfigType(IntEnum): + Routing = 0x00000000 + RxAdapter = 0x00000001 + TxAdapter = 0x00000002 + Channel = 0x0000000CC + ChannelNames = 0x0000000DD + Meta = 0x000000FE + Unknown = 0xFFFFFFFF + + def __bytes__(self): + return self.to_bytes(4, "big") + + +class ConfigError(Exception): + pass + + +@dataclass(frozen=True) +class BlobElement: + description: string + data: Union[bytes, bytearray] + + def __bytes__(self): + return bytes(self.data) + + def __str__(self): + return "".join(self.to_str_list()) + + def to_str_list(self): + return [ + "{data}// {description}\n".format( + data="".join([f"0x{byte:02X}, " for byte in self.data]), + description=self.description, + ) + ] + + +@dataclass(frozen=True) +class BlobElementList: + description: string + data: List[Union[BlobElement, "BlobElementList"]] + + def __bytes__(self): + return bytes(chain.from_iterable(bytes(e) for e in self.data)) + + def __str__(self): + return "".join(self.to_str_list()) + + def to_str_list(self): + return [ + f"// {self.description}\n", + *(f" {s}" for e in self.data for s in e.to_str_list()), + ] diff --git a/tools/blob/__main__.py b/tools/blob/__main__.py new file mode 100644 index 00000000000..23d539b5b63 --- /dev/null +++ b/tools/blob/__main__.py @@ -0,0 +1,330 @@ +# ******************************************************************************* +# Copyright (c) 2026 BMW AG +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import argparse +import importlib +import inspect +import json +import os +import re +import sys +from blob import ( + BlobElement, + BlobElementList, + IConfig, + ConfigType, + ExitCode, +) +from blob.utils import ( + default_or, + iter_to_blob_element_list, + prepend_size, + config, +) +from collections import OrderedDict +from datetime import datetime as dt +from itertools import accumulate, chain + + +class Blob: + VERSION = 1 + MAGIC = 0xDEADBEEF + HEADER_LENGTH = 16 + + def __init__(self, configs=None): + self._version = type(self).VERSION + self._magic = type(self).MAGIC + self._configs = default_or(configs) + + def create_blob_element_list(self): + return BlobElementList( + "Blob", + [ + BlobElement("version", self._version.to_bytes(4, "big")), + BlobElement("magic", self._magic.to_bytes(4, "big")), + prepend_size(BlobElementList("data", [c.create_blob_element_list() for c in self._configs])), + ], + ) + + def __bytes__(self): + return bytes(self.create_blob_element_list()) + + def pprint(self): + return str(self.create_blob_element_list()) + + +class Meta: + def __init__(self, meta_entries): + self._entries = OrderedDict() + for entry in meta_entries: + for key, value in entry.items(): + self._entries[key] = value + "\0" + + def create_blob_element_list(self): + return config( + type=ConfigType.Meta, + data=[ + BlobElement("entry count", len(self._entries).to_bytes(2, "big")), + iter_to_blob_element_list( + [0, *accumulate(map(len, self._entries.values()))], + "entry offsets", + "offset", + ">H", + ), + BlobElementList( + "entries", + [BlobElement(f"{v[:-1]} ({k.replace('-',' ')})", v.encode()) for k, v in self._entries.items()], + ), + ] + if len(self._entries) > 0 + else [], + ) + + +class MetaConfig(IConfig): + @staticmethod + def new(objects): + entries = [o["value"] for o in objects if o["type"] == "meta"] + return (Meta(entries),) if len(entries) > 0 else () + + +class Cli: + @staticmethod + def _create_parser(): + parser = argparse.ArgumentParser(argument_default=argparse.ArgumentDefaultsHelpFormatter, prog="blob") + subparsers = parser.add_subparsers(title="actions", dest="command") + + binary_parser = subparsers.add_parser("binary", help="generates a blob") + pprint_parser = subparsers.add_parser("pprint", help="pretty-prints a blob") + for p in [binary_parser, pprint_parser]: + p.add_argument( + "-i", + "--input", + type=argparse.FileType("r"), + default="-", + help="input file. Defaults to stdin", + ) + p.add_argument( + "-c", + "--config", + nargs="+", + default=[], + help="config(s) for blob", + ) + binary_parser.add_argument( + "-o", + "--output", + default=None, + help="Output file. Defaults to stdout", + ) + + header_parser = subparsers.add_parser("header", help="generates a c++ header file with blob information") + header_subparser = header_parser.add_subparsers(title="actions", dest="subcommand") + data_header_parser = header_subparser.add_parser( + "data", help="generates a c++ header file with a blob data array" + ) + config_type_header_parser = header_subparser.add_parser( + "config-type", help="generates a c++ header file containing the blob config type enum" + ) + metadata_header_parser = header_subparser.add_parser( + "metadata", help="generates a c++ header file with a blob metadata enum" + ) + + data_header_parser.add_argument( + "-i", + "--input", + type=argparse.FileType("rb"), + default=None, + help="blob data. Defaults to stdin", + ) + + metadata_header_parser.add_argument( + "-i", + "--input", + type=argparse.FileType("r"), + default="-", + help="input file. Defaults to stdin", + ) + + for p, default_name, info in [ + (data_header_parser, "BLOB_DATA", "data array"), + (config_type_header_parser, "ConfigType", "config type enum"), + (metadata_header_parser, "Metadata", "metadata enum"), + ]: + p.add_argument( + "-n", + "--name", + dest="name", + default=f"{default_name}", + help=f"Name of the blob {info}. Defaults to {default_name}", + ) + p.add_argument( + "-o", + "--output", + type=argparse.FileType("w"), + default="-", + help="File to which the c/c++ code is written. Defaults to stdout", + ) + + binary_parser.set_defaults(func=Cli.binary) + pprint_parser.set_defaults(func=Cli.pprint) + data_header_parser.set_defaults(func=Cli.data_header) + config_type_header_parser.set_defaults(func=Cli.config_type_header) + metadata_header_parser.set_defaults(func=Cli.metadata_header) + + return parser + + @staticmethod + def create_blob(args): + with args.input as input: + objects = [json.loads(line) for line in input] + dynamic_imports = [importlib.import_module(element) for element in args.config] + configs = [ + *MetaConfig.new(objects), + *chain.from_iterable(imports.Config.new(objects) for imports in dynamic_imports), + ] + return Blob(configs) + + @staticmethod + def binary(args): + blob = Cli.create_blob(args) + if args.output: + with open(args.output, "wb") as fp: + fp.write(bytes(blob)) + else: + with open(sys.stdout.fileno(), mode="wb") as stdout: + stdout.write(bytes(blob)) + return ExitCode.SUCCESS + + @staticmethod + def pprint(args): + blob = Cli.create_blob(args) + print(blob.pprint()) + return ExitCode.SUCCESS + + @staticmethod + def data_header(args): + with open(sys.stdin.fileno(), mode="rb") if args.input is None else args.input as input: + blob_data = input.read() + header_file = ( + inspect.cleandoc( + """ + // Copyright {year} BMW AG + + // This is a generated file. Please do not edit it. + + #pragma once + + #include + + namespace {name_space} {{ + uint8_t const {name}[{size}] = {{ + {content} + }}; + }} // namespace {name_space} + """ + ).format( + year=dt.now().year, + content=", ".join([f"0x{byte:02X}" for byte in blob_data]), + name_space="blob", + name=args.name, + size=len(blob_data), + ) + + "\n" + ) + with args.output as output: + output.write(header_file) + return ExitCode.SUCCESS + + @staticmethod + def config_type_header(args): + config_types = {re.sub(r"(? + + namespace blob + {{ + enum class {name} : uint32_t + {{ + {entries} + }}; + }} // namespace blob + """ + ).format( + year=dt.now().year, + name=args.name, + entries=",\n".join([f"{k} = 0x{v:02X}" for k, v in config_types.items()]), + ) + + "\n" + ) + with args.output as output: + output.write(header_file) + return ExitCode.SUCCESS + + @staticmethod + def metadata_header(args): + with args.input as input: + objects = [json.loads(line) for line in input] + meta_entries = [o["value"] for o in objects if o["type"] == "meta"] + metadata_enum = OrderedDict(dict()) + for e in meta_entries: + for i, k in enumerate(e): + metadata_enum[k.upper().replace("-", "_")] = i + header_file = ( + inspect.cleandoc( + """ + // Copyright {year} BMW AG + + // This is a generated file. Please do not edit it. + + #pragma once + + #include + + namespace blob + {{ + // A blob may contain a metaconfiguration, a list of strings with information + // about other configurations. The name and value of each {name} element specify + // a string and its index in this list respectively. + enum class {name} : uint8_t + {{ + {entries} + }}; + }} // namespace blob + """ + ).format( + year=dt.now().year, + name=args.name, + entries=",\n".join([f"{k} = {v}" for k, v in metadata_enum.items()]), + ) + + "\n" + ) + with args.output as output: + output.write(header_file) + return ExitCode.SUCCESS + + def __init__(self): + self.parser = Cli._create_parser() + + def main(self, argv=None): + args = self.parser.parse_args(argv) + sys.exit(args.func(args)) + + +if __name__ == "__main__": + Cli().main() diff --git a/tools/blob/requirements.txt b/tools/blob/requirements.txt new file mode 100644 index 00000000000..2c601a8eac9 --- /dev/null +++ b/tools/blob/requirements.txt @@ -0,0 +1 @@ +crc==7.1.0 diff --git a/tools/blob/routing/__init__.py b/tools/blob/routing/__init__.py new file mode 100644 index 00000000000..ee627db5f80 --- /dev/null +++ b/tools/blob/routing/__init__.py @@ -0,0 +1,12 @@ +#!/usr/bin/env -S python3 -u +# ******************************************************************************* +# Copyright (c) 2026 BMW AG +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +__all__ = ["table", "utils"] diff --git a/tools/blob/routing/__main__.py b/tools/blob/routing/__main__.py new file mode 100644 index 00000000000..530650bce75 --- /dev/null +++ b/tools/blob/routing/__main__.py @@ -0,0 +1,258 @@ +# ******************************************************************************* +# Copyright (c) 2026 BMW AG +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import argparse +import functools +import inspect +import json +import sys +from blob import ExitCode +from blob.routing.utils import routing2pretty, input_table +from blob.routing.channel import channel_ids +from collections import defaultdict +from datetime import datetime as dt +import sys + + +class Cli: + to_int = functools.partial(int, base=0) + + @staticmethod + def _create_parser(): + parser = argparse.ArgumentParser(argument_default=argparse.ArgumentDefaultsHelpFormatter, prog="routing") + subparsers = parser.add_subparsers(dest="subcommand", description="(Required)", required=True) + + s = subparsers.add_parser( + "sort", + help="Sort jsonl based routings based on their channel IDs", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + s.add_argument("input", nargs="?", type=argparse.FileType("r"), default="-") + s.set_defaults(func=Cli.sort_routings) + + p = subparsers.add_parser( + "pprint", + help="Pretty prints jsonl based routings", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p.add_argument( + "input", + type=argparse.FileType("r"), + nargs="?", + default="-", + help="Jsonl based routings", + ) + p.add_argument( + "-f", + "--format", + choices=["human", "jsonl"], + default="human", + help="which shall be used for the output", + ) + p.set_defaults(func=Cli.pprint) + + v = subparsers.add_parser( + "visualize", + help="Generate a .dot file which visualizes the routing graph for a specific input", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + v.add_argument("--channel", default="", help="channel id of the input message") + v.add_argument("--id", type=Cli.to_int, default=0, help="id of the input message") + v.add_argument("--offset", type=Cli.to_int, default=0, help="offset of the input pdu") + v.add_argument("--pdu-length", type=Cli.to_int, default=8, help="length of the input pdu") + v.add_argument( + "input", + type=argparse.FileType("r"), + nargs="?", + default="-", + help="Jsonl based routings", + ) + v.set_defaults(func=Cli.visualize) + + h = subparsers.add_parser( + "header", + help="Generate a c/c++ header file containing the routing channel IDs", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + h.add_argument( + "input", + type=argparse.FileType("r"), + nargs="?", + default="-", + help="json file containing channel names and IDs", + ) + h.add_argument( + "-o", + "--output", + type=argparse.FileType("w"), + default="-", + help="File to which the c/c++ code is written. Defaults to stdout", + ) + h.set_defaults(func=Cli.header) + + return parser + + @staticmethod + def sort_routings(args): + with args.input as input: + objects = [json.loads(line) for line in input] + channels = [o["value"] for o in objects if o["type"] == "channel"] + static_channel_ids = {c["name"]: c["id"] for c in channels if c["type"] != "pdu_transport"} + pdu_transport_channel_ids = { + c["name"]: i + len(static_channel_ids) for i, c in enumerate(c for c in channels if c["type"] == "pdu_transport") + } + channel_ids = {**static_channel_ids, **pdu_transport_channel_ids} + routings = [o["value"] for o in objects if o["type"] == "routing"] + r = sorted(routings, key=lambda d: channel_ids[d["input"]["channel-name"]]) + for e in r: + print(json.dumps(e)) + return ExitCode.SUCCESS + + @staticmethod + def pprint(args): + with args.input as input: + objects = [json.loads(line) for line in input] + routings = [o["value"] for o in objects if o["type"] == "routing"] + pproutings = routing2pretty(routings) + if args.format == "jsonl": + for routing in pproutings: + print(json.dumps(routing)) + else: + for routing in pproutings: + input = routing["input"] + outputs = routing["outputs"] + print(f'--> [input] {input["channel"]:<10} id: 0x{input["message-id"]:04X} ({input["message-id"]:})') + for output in outputs: + print( + f'<-- [output] {output["channel"]:<10} id: 0x{output["message-id"]:04X} ({output["message-id"]:})' + ) + print() + return ExitCode.SUCCESS + + @staticmethod + def visualize(args): + template = "\n".join( + [ + "digraph Routing {{", + f' graph[fontname="Arial" fontsize="10" ranksep=1.0 nodesep=1.0 label="routing graph for input of Id: {args.id} on Channel: {args.channel}"]', + "{nodes}" "}}", + ] + ) + + with args.input as input: + objects = [json.loads(line) for line in input] + channels = [o["value"] for o in objects if o["type"] == "channel"] + static_channel_ids = {c["name"]: c["id"] for c in channels if c["type"] != "pdu_transport"} + pdu_transport_channel_ids = { + c["name"]: i + len(static_channel_ids) for i, c in enumerate(c for c in channels if c["type"] == "pdu_transport") + } + channel_ids = {**static_channel_ids, **pdu_transport_channel_ids} + channel_names = list(channel_ids.keys()) + routings = [o["value"] for o in objects if o["type"] == "routing"] + table = dict() + for input_channel_id, v in input_table(routings, channel_ids).items(): + table.setdefault(input_channel_id, {}) + for (input_msg_id, _, input_pdu_offset, input_pdu_length), r in v.items(): + table[input_channel_id][(input_msg_id, input_pdu_offset, input_pdu_length)] = r + channel_id = channel_ids[args.channel] + id = args.id + if not (channel_id in table and (args.id, args.offset, args.pdu_length) in table[channel_id]): + print(table[channel_id]) + print( + f"Could not find any routing for input, details: Input[channel({channel_id}), id({id})]", + file=sys.stderr, + ) + return ExitCode.FAILURE + + cycle_detection = defaultdict(lambda: defaultdict(lambda: 0)) + nodes = "" + previous = "Input" + items = [(channel_id, id)] + while len(items) != 0: + channel_id, id = items.pop(0) + nodes += f' "{previous}" -> "{channel_names[channel_id]}" [label = "Id: {id}"]\n' + previous = f"{channel_names[channel_id]}" + if channel_id in table: + if (id, args.offset, args.pdu_length) in table[channel_id]: + if cycle_detection[channel_id][id] != 0: + print("Warning: routing cycle detected!", file=sys.stderr) + continue + items.extend( + (channel_id, id) + for (channel_id, id, _, _) in table[channel_id][(id, args.offset, args.pdu_length)] + ) + cycle_detection[channel_id][id] += 1 + nodes.strip() + print(template.format(nodes=nodes)) + return ExitCode.SUCCESS + + @staticmethod + def header(args): + with args.input as input: + objects = [json.loads(line) for line in input] + + routing_channels = [o["value"] for o in objects if o["type"] == "channel"] + routing_channel_ids = channel_ids(routing_channels) + + static_channels = sorted([c for c in routing_channels if c["type"] != "pdu_transport"], key=lambda x: x["id"]) + can_constants = { + c["name"].replace("-", "_"): routing_channel_ids[c["name"]] for c in static_channels if c["type"] == "can" + } + fr_constants = { + c["name"].replace("-", "_"): routing_channel_ids[c["name"]] + for c in static_channels + if c["type"] == "flexray" + } + + file = ( + inspect.cleandoc( + """ + // Copyright {year} BMW AG + + #pragma once + + namespace routing + {{ + // clang-format off + {channel_ids} + + {can_channels_ids} + + {fr_channels_ids} + // clang-format on + }} // namespace routing + """ + ).format( + year=dt.now().year, + channel_ids="\n".join( + [ + f"static constexpr uint8_t {name} = {value};" + for name, value in sorted({**can_constants, **fr_constants}.items(), key=lambda x: x[1]) + ] + ), + can_channels_ids=f"static constexpr uint8_t canChannelIds[] = {{{', '.join(can_constants)}}};", + fr_channels_ids=f"static constexpr uint8_t frChannelIds[] = {{{', '.join(fr_constants)}}};", + ) + + "\n" + ) + with args.output as output: + output.write(file) + return ExitCode.SUCCESS + + def __init__(self): + self.parser = Cli._create_parser() + + def main(self, argv=None): + args = self.parser.parse_args(argv) + sys.exit(args.func(args)) + + +if __name__ == "__main__": + Cli().main() diff --git a/tools/blob/routing/channel.py b/tools/blob/routing/channel.py new file mode 100644 index 00000000000..1d99f1c359f --- /dev/null +++ b/tools/blob/routing/channel.py @@ -0,0 +1,110 @@ +# ******************************************************************************* +# Copyright (c) 2026 BMW AG +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import enum +from blob import ( + BlobElement, + BlobElementList, + ConfigType, +) +from blob.utils import ( + iter_to_blob_element_list, + config, +) +from itertools import accumulate +from sys import stderr + + +class ChannelType(enum.Enum): + Can = "CAN" + PduTransport = "PDU_TRANSPORT" + FlexRay = "FLEXRAY" + Unsupported = "UNSUPPORTED" + + @staticmethod + def new(value): + if not isinstance(value, str): + raise TypeError("Unsupported type") + key = value.lower() + dispatcher = { + "can": ChannelType.Can, + "pdu_transport": ChannelType.PduTransport, + "flexray": ChannelType.FlexRay, + } + return dispatcher[key] + + def to_json(self): + return self.value.lower() + + def __bytes__(self): + mapping = { + self.PduTransport.value: 0x0000, + self.Can.value: 0x0001, + self.FlexRay.value: 0x0002, + } + return mapping[self.value].to_bytes(2, "big") + + +class ChannelNames: + def __init__(self, channel_ids): + self.channel_ids = channel_ids + + def create_blob_element_list(self): + channel_name_list = [] + previous_id = 0 + for name, id in self.channel_ids.items(): + channel_name_list.extend("" for _ in range(previous_id + 1, id)) + previous_id = id + channel_name_list.append(name + "\0") + + return config( + type=ConfigType.ChannelNames, + data=[ + BlobElement("name count", len(channel_name_list).to_bytes(2, "big")), + iter_to_blob_element_list( + [0, *accumulate(map(len, channel_name_list))], + "channel offsets", + "offset", + ">H", + ), + BlobElementList( + "channel names", + [BlobElement(name[:-1], name.encode()) for name in channel_name_list if len(name) > 0], + ), + ] + if len(channel_name_list) > 0 + else [], + ) + + def __bytes__(self): + return bytes(self.create_blob_element_list()) + + +def channel_ids(routing_channels): + static_channel_ids = dict( + sorted( + [(c["name"], c["id"]) for c in routing_channels if c["type"] != "pdu_transport"], + key=lambda x: x[1], + ) + ) + + if list(static_channel_ids.values()) != list(range(0, len(static_channel_ids))): + print("Invalid static channel IDs", file=stderr) + exit(-1) + + pdu_transport_channel_ids = { + name: len(static_channel_ids) + i + for i, name in enumerate(c["name"] for c in routing_channels if c["type"] == "pdu_transport") + } + + return { + **static_channel_ids, + **pdu_transport_channel_ids, + } diff --git a/tools/blob/routing/table.py b/tools/blob/routing/table.py new file mode 100644 index 00000000000..e0cee44bf20 --- /dev/null +++ b/tools/blob/routing/table.py @@ -0,0 +1,310 @@ +#!/usr/bin/env -S python3 -u + +# ******************************************************************************* +# Copyright (c) 2026 BMW AG +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from ipaddress import ip_address as create_ip_address + +from blob import ( + BlobElement, + ConfigError, + ConfigType, + IConfig, +) +from blob.routing.channel import ChannelType, ChannelNames, channel_ids +from blob.utils import ( + default_or, + iter_to_blob_element_list, + prepend_size, + config, +) +from blob.routing.utils import ( + input_table, + input_pdu_offset_generator, + input_pdu_length_generator, + output_channel_id_generator, + output_message_id_generator, + output_pdu_offset_generator, + output_pdu_length_generator, + destination_offset_generator, + get_first_id, + message_id_generator, + message_length_generator, + pdu_length_offset_generator, +) + + +class RoutingTable: + @staticmethod + def new(table): + return RoutingTable( + destination_offsets=destination_offset_generator(table), + output_message_ids=output_message_id_generator(table), + destinations=output_channel_id_generator(table), + ) + + def __init__(self, destination_offsets=None, output_message_ids=None, destinations=None): + self._destination_offsets = default_or(destination_offsets) + self._output_message_ids = default_or(output_message_ids) + self._destinations = default_or(destinations) + + def create_blob_element_list(self): + return config( + type=ConfigType.Routing, + data=[ + prepend_size( + iter_to_blob_element_list(self._destination_offsets, "destination offsets", "offset", ">I") + ), + prepend_size(iter_to_blob_element_list(self._output_message_ids, "output message ids", "id", ">I")), + prepend_size(iter_to_blob_element_list(self._destinations, "destinations", "destination", ">B")), + ], + ) + + def __bytes__(self): + return bytes(self.create_blob_element_list()) + + +class RxAdapter: + @staticmethod + def new(table, channel_id, channel_type): + return RxAdapter( + channel_id=channel_id, + channel_type=channel_type, + first_id=get_first_id(table, channel_id), + message_ids=message_id_generator(table, channel_id), + message_lengths=message_length_generator(table, channel_id), + pdu_length_offsets=pdu_length_offset_generator(table, channel_id), + pdu_lengths=input_pdu_length_generator(table, channel_id), + pdu_offsets=input_pdu_offset_generator(table, channel_id), + ) + + def __init__( + self, + channel_id, + channel_type=None, + first_id=0, + message_ids=None, + message_lengths=None, + pdu_length_offsets=None, + pdu_lengths=None, + pdu_offsets=None, + ): + self._channel_id = channel_id + self._channel_type = channel_type + self._first_id = first_id + self._message_ids = default_or(message_ids) + self.message_lengths = default_or(message_lengths) + self._pdu_length_offsets = default_or(pdu_length_offsets) + self._pdu_lengths = default_or(pdu_lengths) + self._pdu_offsets = default_or(pdu_offsets) + + def create_blob_element_list(self): + return config( + type=ConfigType.RxAdapter, + data=[ + BlobElement("first id", self._first_id.to_bytes(4, "big")), + prepend_size(iter_to_blob_element_list(self._message_ids, "message ids", "id", ">I")), + prepend_size(iter_to_blob_element_list(self.message_lengths, "message lengths", "length", ">I")), + prepend_size( + iter_to_blob_element_list(self._pdu_length_offsets, "pdu length offsets", "length offset", ">I") + ), + prepend_size(iter_to_blob_element_list(self._pdu_lengths, "pdu lengths", "length", ">I")), + prepend_size(iter_to_blob_element_list(self._pdu_offsets, "pdu offsets", "offset", ">I")), + ], + reserved=[ + BlobElement("channel type", bytes(self._channel_type)), + BlobElement("channel id", self._channel_id.to_bytes(2, "big")), + BlobElement("reserved", bytes(4)), + ], + ) + + def __bytes__(self): + return bytes(self.create_blob_element_list()) + + +class TxAdapter: + @staticmethod + def new(table, channel_id, channel_type): + return TxAdapter( + channel_id=channel_id, + channel_type=channel_type, + message_ids=output_message_id_generator(table, channel_id), + message_lengths=output_pdu_length_generator(table, channel_id), + pdu_offsets=output_pdu_offset_generator(table, channel_id), + ) + + def __init__( + self, + channel_id, + channel_type=None, + message_ids=None, + message_lengths=None, + pdu_offsets=None, + ): + self._channel_id = channel_id + self._channel_type = channel_type + self._message_ids = default_or(message_ids) + self._message_lengths = default_or(message_lengths) + self._pdu_offsets = default_or(pdu_offsets) + + def create_blob_element_list(self): + return config( + type=ConfigType.TxAdapter, + data=[ + prepend_size(iter_to_blob_element_list(self._message_ids, "message ids", "id", ">I")), + prepend_size(iter_to_blob_element_list(self._message_lengths, "message lengths", "length", ">I")), + prepend_size(iter_to_blob_element_list(self._pdu_offsets, "pdu offsets", "offset", ">I")), + ], + reserved=[ + BlobElement("channel type", bytes(self._channel_type)), + BlobElement("channel id", self._channel_id.to_bytes(2, "big")), + BlobElement("reserved", bytes(4)), + ], + ) + + def __bytes__(self): + return bytes(self.create_blob_element_list()) + + +class FlexrayChannelConfig: + def __init__(self, channel_id, config): + self._channel_id = channel_id + self._config = config + + def create_blob_element_list(self): + flexray_fifo_depths = self._config.get("flexray-fifo-depths", {}) + + return config( + type=ConfigType.Channel, + data=[ + BlobElement("pdu count", len(flexray_fifo_depths).to_bytes(1, "big")), + iter_to_blob_element_list(map(int, flexray_fifo_depths.keys()), "pdu ids", "id", ">I"), + iter_to_blob_element_list(flexray_fifo_depths.values(), "depths", "depth", ">B"), + ], + reserved=[ + BlobElement("channel type", bytes(ChannelType.FlexRay)), + BlobElement("channel id", self._channel_id.to_bytes(2, "big")), + BlobElement("reserved", bytes(4)), + ], + ) + + def __bytes__(self): + return bytes(self.create_blob_element_list()) + + +class PduTransportChannelConfig: + TYPE_MAPPING = {"multicast": 0x01} + MODE_MAPPING = {"rx": 0x01, "tx": 0x10, "rxtx": 0x11} + + def __init__(self, channel_id, config): + self.check_config(config) + self._channel_id = channel_id + self._config = config + + @staticmethod + def check_config(cfg): + type = cfg["connection-type"] + if type not in ["multicast"]: + raise ConfigError("Unsupported pdu_transport config type") + try: + cfg.setdefault("mode", "rxtx") + cfg.setdefault("vlan-id", 0) + keys_for = {"multicast": ["mode", "local-port", "remote-port", "vlan-id", "ip-address"]} + for key in keys_for[type]: + cfg[key] + except KeyError as ex: + raise ConfigError("Config setting") from ex + + def create_blob_element_list(self): + connection_type = PduTransportChannelConfig.TYPE_MAPPING[self._config["connection-type"]] + mode = PduTransportChannelConfig.MODE_MAPPING[self._config["mode"]] + vlan_id = self._config["vlan-id"] + local_port = self._config["local-port"] + remote_port = self._config["remote-port"] + ip_address = create_ip_address(self._config["ip-address"]) + pcp = self._config["pcp"] + transmission_timeout = self._config.get("transmission-timeout", 0) + remote_ip_addresses = [create_ip_address(a) for a in self._config.get("remote-ip-addresses", [])] + + return config( + type=ConfigType.Channel, + data=[ + BlobElement("connection type", connection_type.to_bytes(1, "big")), + BlobElement("mode", mode.to_bytes(1, "big")), + BlobElement("vlan id", vlan_id.to_bytes(2, "big")), + BlobElement("local port", local_port.to_bytes(2, "big")), + BlobElement("remote port", remote_port.to_bytes(2, "big")), + BlobElement("ip version", ip_address.version.to_bytes(2, "big")), + BlobElement("ip address", ip_address.packed), + BlobElement("pcp", pcp.to_bytes(1, "big")), + BlobElement("transmission timeout", transmission_timeout.to_bytes(2, "big")), + BlobElement("remote ip address count", len(remote_ip_addresses).to_bytes(1, "big")), + iter_to_blob_element_list( + [a.packed for a in remote_ip_addresses], + "remote ip addresses", + "address", + ), + ], + reserved=[ + BlobElement("channel type", bytes(ChannelType.PduTransport)), + BlobElement("channel id", self._channel_id.to_bytes(2, "big")), + BlobElement("reserved", bytes(4)), + ], + ) + + def __bytes__(self): + return bytes(self.create_blob_element_list()) + + +class Config(IConfig): + @staticmethod + def new(objects): + routings = [o["value"] for o in objects if o["type"] == "routing"] + routing_channels = [o["value"] for o in objects if o["type"] == "channel"] + + routing_channel_ids = channel_ids(routing_channels) + table = input_table(routings, routing_channel_ids) + entries = ( + RoutingTable.new(table), + *( + RxAdapter.new( + table, + routing_channel_ids[channel["name"]], + ChannelType.new(channel["type"]), + ) + for channel in routing_channels + ), + *( + TxAdapter.new( + table, + routing_channel_ids[channel["name"]], + ChannelType.new(channel["type"]), + ) + for channel in routing_channels + ), + *( + PduTransportChannelConfig( + routing_channel_ids[channel["name"]], + channel["configuration"], + ) + for channel in routing_channels + if channel["type"] == "pdu_transport" + ), + *( + FlexrayChannelConfig( + routing_channel_ids[channel["name"]], + channel["configuration"], + ) + for channel in routing_channels + if channel["type"] == "flexray" + ), + ChannelNames(routing_channel_ids), + ) + return entries diff --git a/tools/blob/routing/utils.py b/tools/blob/routing/utils.py new file mode 100644 index 00000000000..f2ea334ab03 --- /dev/null +++ b/tools/blob/routing/utils.py @@ -0,0 +1,144 @@ +#!/usr/bin/env -S python3 -u + +# ******************************************************************************* +# Copyright (c) 2026 BMW AG +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from collections import defaultdict, OrderedDict + + +def routing2pretty(routings): + for r in routings: + input = r["input"] + outputs = r["outputs"] + input = {"channel": input["channel-name"], "message-id": input["message-id"]} + outputs = [{"channel": output["channel-name"], "message-id": output["message-id"]} for output in outputs] + yield {"input": input, "outputs": outputs} + + +def input_table(routings, channel_ids): + for r in routings: + r["input"]["channel-id"] = channel_ids[r["input"].pop("channel-name")] + for o in r["outputs"]: + o["channel-id"] = channel_ids[o.pop("channel-name")] + + table = defaultdict(dict) + for _, channel_id in channel_ids.items(): + table.setdefault(channel_id, {}) + for r in routings: + input_channel_id = r["input"]["channel-id"] + input_msg_id = r["input"]["message-id"] + input_msg_length = r["input"].get("message-length", 0) + input_pdu_offset = r["input"]["offset"] + input_pdu_length = r["input"]["length"] + outputs = [ + ( + output["channel-id"], + output["message-id"], + output.get("offset", 0), + output.get("length", input_pdu_length), + ) + for output in r["outputs"] + ] + table[input_channel_id][(input_msg_id, input_msg_length, input_pdu_offset, input_pdu_length)] = outputs + table = OrderedDict(dict(sorted(table.items(), key=lambda x: x))) + for b, d in table.items(): + d = OrderedDict(dict(sorted(d.items(), key=lambda x: (x[0][0], x[0][1], x[0][2], -x[0][3])))) + table[b] = d + return table + + +def input_message_id_generator(table, channel_id): + yield from (msg_id for (msg_id, _, _, _), _ in table[channel_id].items()) + + +def input_message_length_generator(table, channel_id): + yield from (msg_length for (_, msg_length, _, _), _ in table[channel_id].items()) + + +def input_pdu_offset_generator(table, channel_id): + yield from (pdu_offset for (_, _, pdu_offset, _), _ in table[channel_id].items()) + + +def input_pdu_length_generator(table, channel_id): + yield from (pdu_length for (_, _, _, pdu_length), _ in table[channel_id].items()) + + +def output_channel_id_generator(table): + for routings in table.values(): + for outputs in routings.values(): + yield from (channel_id for channel_id, _, _, _ in outputs) + + +def output_message_id_generator(table, channel_id=None): + for routings in table.values(): + for outputs in routings.values(): + yield from (o_msg_id for o_ch_id, o_msg_id, _, _ in outputs if channel_id in [None, o_ch_id]) + + +def output_pdu_offset_generator(table, channel_id): + for routings in table.values(): + for outputs in routings.values(): + yield from (o_offset for o_ch_id, _, o_offset, _ in outputs if o_ch_id == channel_id) + + +def output_pdu_length_generator(table, channel_id): + for routings in table.values(): + for outputs in routings.values(): + yield from (o_length for o_ch_id, _, _, o_length in outputs if o_ch_id == channel_id) + + +def destination_offset_generator(table): + offset = 0 + yield offset + for routings in table.values(): + for outputs in routings.values(): + offset += len(outputs) + yield offset + + +def get_first_id(table, channel_id): + first_id = 0 + for input_channel_id, routings in table.items(): + if input_channel_id == channel_id: + return first_id + first_id += len(routings) + return first_id + + +def message_id_generator(table, channel_id): + prev = None + for r in input_message_id_generator(table, channel_id): + if r != prev: + prev = r + yield r + + +def message_length_generator(table, channel_id): + input_message_ids = list(input_message_id_generator(table, channel_id)) + non_zero_input_message_lengths = list( + length for length in input_message_length_generator(table, channel_id) if length > 0 + ) + if len(input_message_ids) == len(non_zero_input_message_lengths): + prev = None + for i, r in enumerate(input_message_ids): + if r != prev: + prev = r + yield non_zero_input_message_lengths[i] + + +def pdu_length_offset_generator(table, channel_id): + offset = 0 + prev = None + for (msg_id, _, _, _), _ in table[channel_id].items(): + if msg_id != prev: + prev = msg_id + yield offset + offset += 1 + yield offset diff --git a/tools/blob/utils.py b/tools/blob/utils.py new file mode 100644 index 00000000000..fbb4657720e --- /dev/null +++ b/tools/blob/utils.py @@ -0,0 +1,101 @@ +#!/usr/bin/env -S python3 -u +# ******************************************************************************* +# Copyright (c) 2026 BMW AG +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import struct +from blob import ( + BlobElement, + BlobElementList, +) +from crc import Calculator, Crc32 + +calculator = Calculator(Crc32.CRC32) + + +def default_or(iterable, default=lambda: []): + """Construct a list from iterable or a default if the iterable is None""" + return list(iterable) if iterable else default() + + +def make_padding(data_length, byte=0xFF, line_length=4): + return bytes(byte for _ in range(data_length % line_length, line_length if data_length % line_length > 0 else 0)) + + +def blob_element_generator(iterable, element_name, element_fmt=None): + for index, element in enumerate(iterable): + yield BlobElement( + description=f"{element_name} {index}", + data=struct.pack(element_fmt, element) if element_fmt else bytes(element), + ) + + +def iter_to_blob_element_list(iterable, list_name, element_name, element_fmt=None): + return BlobElementList(list_name, list(blob_element_generator(iterable, element_name, element_fmt))) + + +def prepend_size(blob_element_list, extra_size=0): + return BlobElementList( + blob_element_list.description, + [ + BlobElement("size", struct.pack(">I", len(bytes(blob_element_list)) + extra_size)), + *blob_element_list.data, + ], + ) + + +def append_padding(blob_element_list, byte=0xFF, line_length=4): + padding = make_padding(len(bytes(blob_element_list)), byte, line_length) + return ( + BlobElementList( + blob_element_list.description, + [ + *blob_element_list.data, + BlobElement("padding", padding), + ], + ) + if len(padding) > 0 + else blob_element_list + ) + + +def validate_blob_element_list(blob_element_list): + """Compute CRC on "data" of BlobElementList with description "" + and append to "data" of BlobElementList with description "data" + (which is, at the last index of 's "data")""" + blob_element_list.data[-1].data.append( + BlobElement("crc", calculator.checksum(bytes(blob_element_list)).to_bytes(4, "big")) + ) + + return blob_element_list + + +def config(type, data=None, reserved=None): + NUMBER_OF_RESERVED_BYTES = 8 + NUMBER_OF_CRC_BYTES = 4 + + reserved_size = 0 if reserved is None else sum(map(len, [bytes(e) for e in reserved]), 0) + + return validate_blob_element_list( + BlobElementList( + type.name, + [ + BlobElement("type", bytes(type)), + *( + [BlobElement("reserved", bytes(NUMBER_OF_RESERVED_BYTES))] + if reserved is None or reserved_size != NUMBER_OF_RESERVED_BYTES + else reserved + ), + prepend_size( + append_padding(BlobElementList("data", [] if data is None else data)), + NUMBER_OF_CRC_BYTES, + ), + ], + ) + )