├── .gitignore ├── data ├── images │ ├── logo.gif │ ├── NUCLEO-H743ZI.png │ ├── transmission.png │ └── bidibit_duration.png └── transmission.pu ├── examples ├── esp32 │ ├── .gitignore │ ├── main │ │ ├── CMakeLists.txt │ │ ├── idf_component.yml │ │ └── app_main.cpp │ ├── CMakeLists.txt │ └── README.md ├── repl │ ├── src │ │ ├── command_station.cpp │ │ ├── command_station.hpp │ │ ├── fifo.hpp │ │ ├── decoder.hpp │ │ ├── decoder.cpp │ │ └── main.cpp │ └── CMakeLists.txt ├── stm32 │ ├── src │ │ ├── command_station.hpp │ │ ├── bsp.h │ │ ├── decoder.hpp │ │ ├── decoder.cpp │ │ └── command_station.cpp │ ├── CMakeLists.txt │ └── STM32H743ZITX_FLASH.ld └── CMakeLists.txt ├── tests ├── bidi │ ├── encode_decode_test.cpp │ ├── datagram_size.cpp │ ├── encode_decode_test.hpp │ ├── encode_decode.cpp │ └── dissector.cpp ├── rx │ ├── service_mode.cpp │ ├── backoff_test.hpp │ ├── backoff_test.cpp │ ├── receive.cpp │ ├── broadcast.cpp │ ├── rx_mock.hpp │ ├── backoff.cpp │ ├── feature_expansion.cpp │ ├── scale_speed.cpp │ ├── speed_direction.cpp │ ├── cv_short.cpp │ ├── function_group.cpp │ ├── consist_control.cpp │ ├── decoder_lock.cpp │ ├── advanced_operations.cpp │ ├── consist.cpp │ ├── app_dyn.cpp │ ├── app_tos.cpp │ ├── app_pom.cpp │ ├── rx_test.cpp │ ├── rx_test.hpp │ ├── app_adr.cpp │ ├── logon.cpp │ └── cv_long.cpp ├── tx │ ├── capacity.cpp │ ├── bytes.cpp │ ├── packet.cpp │ ├── init.cpp │ ├── size.cpp │ ├── tx_mock.hpp │ ├── tx_test.hpp │ ├── service.cpp │ ├── timings_adapter.cpp │ ├── tx_test.cpp │ └── transmit.cpp ├── instruction.cpp ├── utility.hpp ├── CMakeLists.txt ├── crc8.cpp └── address.cpp ├── .github └── workflows │ ├── license.yml │ ├── release.yml │ ├── tests.yml │ └── build.yml ├── .licenserc.json ├── idf_component.yml ├── CMakePresets.json ├── TODO.md ├── include ├── dcc │ ├── bit.hpp │ ├── dcc.hpp │ ├── bidi │ │ ├── baudrate.hpp │ │ ├── app │ │ │ ├── ext.hpp │ │ │ ├── info.hpp │ │ │ ├── zeit.hpp │ │ │ ├── block.hpp │ │ │ ├── cv_auto.hpp │ │ │ ├── pom.hpp │ │ │ ├── srq.hpp │ │ │ ├── adr_low.hpp │ │ │ ├── dyn.hpp │ │ │ ├── adr_high.hpp │ │ │ └── xpom.hpp │ │ ├── nak.hpp │ │ ├── direction_status_byte.hpp │ │ ├── acks.hpp │ │ ├── track_voltage.hpp │ │ ├── temperature.hpp │ │ ├── kmh.hpp │ │ ├── channel.hpp │ │ ├── timing.hpp │ │ └── datagram.hpp │ ├── speed.hpp │ ├── direction.hpp │ ├── packet.hpp │ ├── addresses.hpp │ ├── rx │ │ ├── high_current.hpp │ │ ├── east_west.hpp │ │ ├── async_readable.hpp │ │ ├── async_writable.hpp │ │ ├── writable.hpp │ │ ├── readable.hpp │ │ ├── backoff.hpp │ │ ├── decoder.hpp │ │ └── timing.hpp │ ├── address_group.hpp │ ├── address_assign.hpp │ ├── tx │ │ ├── command_station.hpp │ │ ├── config.hpp │ │ ├── timing.hpp │ │ ├── timings.hpp │ │ ├── timings_adapter.hpp │ │ └── crtp_base.hpp │ ├── exor.hpp │ ├── instruction.hpp │ ├── crc8.hpp │ └── address.hpp └── rmt_dcc_encoder.h ├── .vscode ├── launch.json └── tasks.json ├── CMakeLists.txt └── .clang-format /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build -------------------------------------------------------------------------------- /data/images/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZIMO-Elektronik/DCC/HEAD/data/images/logo.gif -------------------------------------------------------------------------------- /data/images/NUCLEO-H743ZI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZIMO-Elektronik/DCC/HEAD/data/images/NUCLEO-H743ZI.png -------------------------------------------------------------------------------- /data/images/transmission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZIMO-Elektronik/DCC/HEAD/data/images/transmission.png -------------------------------------------------------------------------------- /examples/esp32/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | managed_components 3 | dependencies.lock 4 | sdkconfig 5 | sdkconfig.old -------------------------------------------------------------------------------- /data/images/bidibit_duration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZIMO-Elektronik/DCC/HEAD/data/images/bidibit_duration.png -------------------------------------------------------------------------------- /examples/esp32/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB_RECURSE SRC *.cpp) 2 | idf_component_register(SRCS ${SRC} INCLUDE_DIRS .) 3 | -------------------------------------------------------------------------------- /examples/esp32/main/idf_component.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | ZIMO-Elektronik/DCC: 3 | version: "*" 4 | override_path: '../../../' -------------------------------------------------------------------------------- /tests/bidi/encode_decode_test.cpp: -------------------------------------------------------------------------------- 1 | #include "encode_decode_test.hpp" 2 | 3 | EncodeDecodeTest::EncodeDecodeTest() {} 4 | 5 | EncodeDecodeTest::~EncodeDecodeTest() {} 6 | -------------------------------------------------------------------------------- /examples/esp32/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25) 2 | 3 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 4 | project(DCCEsp32Rmt LANGUAGES ASM C CXX) 5 | -------------------------------------------------------------------------------- /tests/rx/service_mode.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | // Enter service mode when receiving a reset packet 4 | TEST_F(RxTest, enter_service_mode) { EnterServiceMode(); } 5 | -------------------------------------------------------------------------------- /tests/tx/capacity.cpp: -------------------------------------------------------------------------------- 1 | #include "tx_test.hpp" 2 | 3 | TEST_F(TxTest, capacity) { 4 | EXPECT_ALL_EQ( 5 | DCC_TX_DEQUE_SIZE, _packet_mock.capacity(), _timings_mock.capacity()); 6 | } 7 | -------------------------------------------------------------------------------- /tests/tx/bytes.cpp: -------------------------------------------------------------------------------- 1 | #include "tx_test.hpp" 2 | 3 | TEST_F(TxTest, bytes) { 4 | auto packet{dcc::make_idle_packet()}; 5 | EXPECT_ALL_TRUE(_packet_mock.bytes(packet), _timings_mock.bytes(packet)); 6 | } 7 | -------------------------------------------------------------------------------- /tests/tx/packet.cpp: -------------------------------------------------------------------------------- 1 | #include "tx_test.hpp" 2 | 3 | TEST_F(TxTest, packet) { 4 | auto packet{dcc::make_idle_packet()}; 5 | EXPECT_ALL_TRUE(_packet_mock.packet(packet), _timings_mock.packet(packet)); 6 | } 7 | -------------------------------------------------------------------------------- /examples/repl/src/command_station.cpp: -------------------------------------------------------------------------------- 1 | #include "command_station.hpp" 2 | 3 | void CommandStation::biDiStart() {} 4 | 5 | void CommandStation::biDiChannel1() {} 6 | 7 | void CommandStation::biDiChannel2() {} 8 | 9 | void CommandStation::biDiEnd() {} 10 | -------------------------------------------------------------------------------- /.github/workflows/license.yml: -------------------------------------------------------------------------------- 1 | name: license 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | license: 11 | uses: ZIMO-Elektronik/.github-workflows/.github/workflows/license-checker.yml@v0.2.0 12 | -------------------------------------------------------------------------------- /tests/tx/init.cpp: -------------------------------------------------------------------------------- 1 | #include "tx_test.hpp" 2 | 3 | TEST_F(TxTest, init) { 4 | dcc::tx::Config cfg{ 5 | .num_preamble = 1u, .bit1_duration = 51u, .bit0_duration = 113u}; 6 | ASSERT_DEBUG_DEATH(_packet_mock.init(cfg), ".*"); 7 | ASSERT_DEBUG_DEATH(_timings_mock.init(cfg), ".*"); 8 | } 9 | -------------------------------------------------------------------------------- /.licenserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "{include,src}/**/*.{c,cpp,h,hpp}": [ 3 | "// This Source Code Form is subject to the terms of the Mozilla Public", 4 | "// License, v. 2.0. If a copy of the MPL was not distributed with this", 5 | "// file, You can obtain one at https://mozilla.org/MPL/2.0/." 6 | ] 7 | } -------------------------------------------------------------------------------- /examples/stm32/src/command_station.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct CommandStation : dcc::tx::CrtpBase { 6 | friend dcc::tx::CrtpBase; 7 | 8 | private: 9 | // Write track outputs 10 | void trackOutputs(bool N, bool P); 11 | }; 12 | -------------------------------------------------------------------------------- /examples/repl/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB_RECURSE SRC src/*.cpp) 2 | add_executable(DCCRepl ${SRC}) 3 | 4 | target_common_warnings(DCCRepl PRIVATE) 5 | target_common_errors(DCCRepl PRIVATE) 6 | 7 | cpmaddpackage("gh:daniele77/cli@2.2.0") 8 | 9 | target_link_libraries(DCCRepl PRIVATE cli::cli DCC::DCC) 10 | -------------------------------------------------------------------------------- /tests/rx/backoff_test.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct BackoffTest : ::testing::Test { 8 | BackoffTest(); 9 | virtual ~BackoffTest(); 10 | 11 | int CountTillFalse(int max); 12 | 13 | dcc::rx::Backoff _backoff{}; 14 | }; 15 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(ESP_PLATFORM) 2 | # ESP32 example gets automatically included by component manager 3 | # https://docs.espressif.com/projects/idf-component-manager/en/latest/reference/manifest_file.html#examples 4 | elseif(CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME) 5 | add_subdirectory(repl) 6 | else() 7 | add_subdirectory(stm32) 8 | endif() 9 | -------------------------------------------------------------------------------- /tests/tx/size.cpp: -------------------------------------------------------------------------------- 1 | #include "tx_test.hpp" 2 | 3 | TEST_F(TxTest, initial_size) { 4 | EXPECT_ALL_EQ(0uz, _packet_mock.size(), _timings_mock.size()); 5 | } 6 | 7 | TEST_F(TxTest, size) { 8 | _packet_mock.packet(dcc::make_idle_packet()); 9 | _timings_mock.packet(dcc::make_idle_packet()); 10 | EXPECT_ALL_EQ(1uz, _packet_mock.size(), _timings_mock.size()); 11 | } 12 | -------------------------------------------------------------------------------- /idf_component.yml: -------------------------------------------------------------------------------- 1 | description: DCC protocol for controlling digital model railways 2 | url: http://zimo.at 3 | documentation: https://github.com/ZIMO-Elektronik/DCC/blob/master/README.md 4 | repository: https://github.com/ZIMO-Elektronik/DCC.git 5 | issues: https://github.com/ZIMO-Elektronik/DCC/issues 6 | 7 | maintainers: 8 | - "Vincent Hamp " 9 | 10 | dependencies: 11 | idf: ">=5.1.0" 12 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 6, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 25, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "Debug", 11 | "binaryDir": "${sourceDir}/build", 12 | "cacheVariables": { 13 | "CMAKE_BUILD_TYPE": "Debug" 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /tests/bidi/datagram_size.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | using namespace dcc::bidi; 6 | 7 | TEST(bidi, datagram_size) { 8 | EXPECT_EQ(datagram_size, 2uz); 9 | EXPECT_EQ(datagram_size, 3uz); 10 | EXPECT_EQ(datagram_size, 4uz); 11 | EXPECT_EQ(datagram_size, 6uz); 12 | EXPECT_EQ(datagram_size, 8uz); 13 | } 14 | -------------------------------------------------------------------------------- /examples/repl/src/command_station.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct CommandStation : dcc::tx::CrtpBase { 6 | friend dcc::tx::CrtpBase; 7 | 8 | private: 9 | // BiDi start 10 | void biDiStart(); 11 | 12 | // BiDi channel 1 13 | void biDiChannel1(); 14 | 15 | // BiDi channel 2 16 | void biDiChannel2(); 17 | 18 | // BiDi end 19 | void biDiEnd(); 20 | }; 21 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - Literal for converting CV number to index? The "- 1u" everything is fucking ugly 2 | - dcc::tx::CrtpBase currently pops it's deque at a bad time. Although the design or inplace_deque guarantees that it's not UB it looks funky. 3 | - Replace `RMT_MEM_ALLOC_CAPS` macro with [`rmt_alloc_encoder_mem`](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/rmt.html#_CPPv421rmt_alloc_encoder_mem6size_t) 4 | - Add STM32 examples? -------------------------------------------------------------------------------- /tests/bidi/encode_decode_test.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace dcc::bidi; 8 | 9 | // BiDi encode-decode test fixture 10 | struct EncodeDecodeTest : ::testing::Test { 11 | protected: 12 | EncodeDecodeTest(); 13 | virtual ~EncodeDecodeTest(); 14 | }; 15 | 16 | MATCHER_P(DatagramMatcher, datagram, "") { 17 | return std::ranges::equal(datagram, arg); 18 | } 19 | -------------------------------------------------------------------------------- /tests/rx/backoff_test.cpp: -------------------------------------------------------------------------------- 1 | #include "backoff_test.hpp" 2 | #include 3 | 4 | BackoffTest::BackoffTest() { 5 | std::random_device rd; 6 | std::mt19937 gen(rd()); 7 | std::uniform_int_distribution dist(0, RAND_MAX); 8 | srand(dist(gen)); 9 | } 10 | 11 | BackoffTest::~BackoffTest() {} 12 | 13 | int BackoffTest::CountTillFalse(int max) { 14 | int i{}; 15 | for (; i < max; ++i) 16 | if (!_backoff) break; 17 | return i; 18 | } 19 | -------------------------------------------------------------------------------- /tests/tx/tx_mock.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | template 8 | struct TxMock : dcc::tx::CrtpBase, T> { 9 | MOCK_METHOD(void, trackOutputs, (bool, bool)); 10 | MOCK_METHOD(void, packetEnd, ()); 11 | MOCK_METHOD(void, biDiStart, ()); 12 | MOCK_METHOD(void, biDiChannel1, ()); 13 | MOCK_METHOD(void, biDiChannel2, ()); 14 | MOCK_METHOD(void, biDiEnd, ()); 15 | }; 16 | -------------------------------------------------------------------------------- /include/dcc/bit.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Bit 6 | /// 7 | /// \file dcc/bit.hpp 8 | /// \author Vincent Hamp 9 | /// \date 29/11/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc { 16 | 17 | enum Bit : uint8_t { _0, _1, Invalid }; 18 | 19 | } // namespace dcc 20 | -------------------------------------------------------------------------------- /include/dcc/dcc.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// DCC 6 | /// 7 | /// \file dcc/dcc.hpp 8 | /// \author Vincent Hamp 9 | /// \date 04/01/2022 10 | 11 | #pragma once 12 | 13 | #include "bidi/baudrate.hpp" 14 | #include "bidi/dissector.hpp" 15 | #include "rx/crtp_base.hpp" 16 | #include "tx/crtp_base.hpp" 17 | -------------------------------------------------------------------------------- /include/dcc/bidi/baudrate.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi baudrate 6 | /// 7 | /// \file dcc/bidi/baudrate.hpp 8 | /// \author Vincent Hamp 9 | /// \date 12/02/2023 10 | 11 | #pragma once 12 | 13 | namespace dcc::bidi { 14 | 15 | /// Baudrate 16 | inline constexpr auto baudrate{250'000u}; 17 | 18 | } // namespace dcc::bidi 19 | -------------------------------------------------------------------------------- /include/dcc/speed.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Speed 6 | /// 7 | /// \file dcc/speed.hpp 8 | /// \author Vincent Hamp 9 | /// \date 06/06/2024 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc { 16 | 17 | enum Speed : int32_t { 18 | EStop = -1, 19 | Stop = 0, 20 | }; 21 | 22 | } // namespace dcc 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | upload_components: 10 | runs-on: ubuntu-24.04 11 | steps: 12 | - uses: actions/checkout@v5.0.0 13 | with: 14 | fetch-depth: 0 15 | - uses: espressif/upload-components-ci-action@v1 16 | with: 17 | name: DCC 18 | version: ${{ github.ref_name }} 19 | namespace: zimo-elektronik 20 | api_token: ${{ secrets.IDF_COMPONENT_API_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | tests: 11 | strategy: 12 | matrix: 13 | compliance: [OFF, ON] 14 | uses: ZIMO-Elektronik/.github-workflows/.github/workflows/x86_64-linux-gnu-gcc.yml@v0.2.0 15 | with: 16 | args: -DCMAKE_BUILD_TYPE=Debug -DDCC_STANDARD_COMPLIANCE=${{ matrix.compliance }} 17 | target: DCCTests 18 | post-build: ctest --test-dir build --schedule-random --timeout 86400 19 | -------------------------------------------------------------------------------- /include/dcc/direction.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Direction 6 | /// 7 | /// \file dcc/direction.hpp 8 | /// \author Vincent Hamp 9 | /// \date 06/06/2024 10 | 11 | #pragma once 12 | 13 | namespace dcc { 14 | 15 | enum Direction : bool { 16 | Forward = true, 17 | Backward = false, 18 | East = true, 19 | West = false, 20 | }; 21 | 22 | } // namespace dcc 23 | -------------------------------------------------------------------------------- /include/dcc/packet.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Packet 6 | /// 7 | /// \file dcc/packet.hpp 8 | /// \author Vincent Hamp 9 | /// \date 31/01/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | 16 | namespace dcc { 17 | 18 | using Packet = ztl::inplace_vector; 19 | 20 | } // namespace dcc 21 | -------------------------------------------------------------------------------- /include/dcc/addresses.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Addresses 6 | /// 7 | /// \file dcc/addresses.hpp 8 | /// \author Vincent Hamp 9 | /// \date 04/01/2022 10 | 11 | #pragma once 12 | 13 | #include "address.hpp" 14 | 15 | namespace dcc { 16 | 17 | struct Addresses { 18 | Address primary{}; 19 | Address consist{}; 20 | Address logon{}; 21 | Address received{}; 22 | }; 23 | 24 | } // namespace dcc 25 | -------------------------------------------------------------------------------- /tests/tx/tx_test.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "../utility.hpp" 5 | #include "tx_mock.hpp" 6 | 7 | using namespace ::testing; 8 | 9 | // Transmit test fixture 10 | struct TxTest : ::testing::Test { 11 | TxTest(); 12 | virtual ~TxTest(); 13 | 14 | void SetUp() override; 15 | 16 | dcc::tx::Config _cfg{.num_preamble = DCC_TX_MIN_PREAMBLE_BITS + 1u, 17 | .bit1_duration = 59u, 18 | .bit0_duration = 113u}; 19 | NiceMock> _packet_mock; 20 | NiceMock> _timings_mock; 21 | }; 22 | -------------------------------------------------------------------------------- /include/dcc/bidi/app/ext.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:ext 6 | /// 7 | /// \file dcc/bidi/app/ext.hpp 8 | /// \author Vincent Hamp 9 | /// \date 17/06/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::bidi::app { 16 | 17 | struct Ext { 18 | static constexpr uint8_t id{3u}; 19 | constexpr bool operator==(Ext const&) const = default; 20 | }; 21 | 22 | } // namespace dcc::bidi::app 23 | -------------------------------------------------------------------------------- /include/dcc/bidi/app/info.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:info 6 | /// 7 | /// \file dcc/bidi/app/info.hpp 8 | /// \author Vincent Hamp 9 | /// \date 17/06/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::bidi::app { 16 | 17 | struct Info { 18 | static constexpr uint8_t id{4u}; 19 | constexpr bool operator==(Info const&) const = default; 20 | }; 21 | 22 | } // namespace dcc::bidi::app 23 | -------------------------------------------------------------------------------- /include/dcc/bidi/app/zeit.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:zeit 6 | /// 7 | /// \file dcc/bidi/app/zeit.hpp 8 | /// \author Vincent Hamp 9 | /// \date 17/06/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::bidi::app { 16 | 17 | struct Zeit { 18 | static constexpr uint8_t id{14u}; 19 | constexpr bool operator==(Zeit const&) const = default; 20 | }; 21 | 22 | } // namespace dcc::bidi::app 23 | -------------------------------------------------------------------------------- /include/dcc/rx/high_current.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// High current 6 | /// 7 | /// \file dcc/rx/high_current.hpp 8 | /// \author Vincent Hamp 9 | /// \date 29/11/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::rx { 16 | 17 | template 18 | concept HighCurrent = requires(T t, bool high_current) { 19 | { t.highCurrentBiDi(high_current) }; 20 | }; 21 | 22 | } // namespace dcc::rx 23 | -------------------------------------------------------------------------------- /include/dcc/bidi/app/block.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:block 6 | /// 7 | /// \file dcc/bidi/app/block.hpp 8 | /// \author Vincent Hamp 9 | /// \date 17/06/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::bidi::app { 16 | 17 | struct Block { 18 | static constexpr uint8_t id{13u}; 19 | constexpr bool operator==(Block const&) const = default; 20 | }; 21 | 22 | } // namespace dcc::bidi::app 23 | -------------------------------------------------------------------------------- /include/dcc/bidi/nak.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi NAK 6 | /// 7 | /// \file dcc/bidi/nak.hpp 8 | /// \author Vincent Hamp 9 | /// \date 15/05/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | 16 | namespace dcc::bidi { 17 | 18 | using Nak = uint8_t; 19 | 20 | /// Instruction received correctly but not supported 21 | inline constexpr uint8_t nak{0b0011'1100u}; 22 | 23 | } // namespace dcc::bidi 24 | -------------------------------------------------------------------------------- /include/dcc/address_group.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Address group 6 | /// 7 | /// \file dcc/address_group.hpp 8 | /// \author Vincent Hamp 9 | /// \date 02/04/2024 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc { 16 | 17 | /// Address group (RCN-218) 18 | enum struct AddressGroup : uint8_t { 19 | All = 0b00u, 20 | Loco = 0b01u, 21 | Acc = 0b10u, 22 | Now = 0b11u 23 | }; 24 | 25 | } // namespace dcc 26 | -------------------------------------------------------------------------------- /include/dcc/bidi/app/cv_auto.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:CV-auto 6 | /// 7 | /// \file dcc/bidi/app/cv_auto.hpp 8 | /// \author Vincent Hamp 9 | /// \date 17/06/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::bidi::app { 16 | 17 | struct CvAuto { 18 | static constexpr uint8_t id{12u}; 19 | constexpr bool operator==(CvAuto const&) const = default; 20 | }; 21 | 22 | } // namespace dcc::bidi::app 23 | -------------------------------------------------------------------------------- /include/dcc/bidi/app/pom.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:pom 6 | /// 7 | /// \file dcc/bidi/app/pom.hpp 8 | /// \author Vincent Hamp 9 | /// \date 17/06/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::bidi::app { 16 | 17 | struct Pom { 18 | static constexpr uint8_t id{0u}; 19 | uint8_t d{}; 20 | constexpr bool operator==(Pom const&) const = default; 21 | }; 22 | 23 | } // namespace dcc::bidi::app 24 | -------------------------------------------------------------------------------- /include/dcc/address_assign.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Address assign 6 | /// 7 | /// \file dcc/address_assign.hpp 8 | /// \author Vincent Hamp 9 | /// \date 17/05/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc { 16 | 17 | /// Address assign (RCN-218) 18 | enum struct AddressAssign : uint8_t { 19 | Reserved = 0b00u | 0b01u, 20 | Permanent = 0b10u, 21 | Temporary = 0b11u, 22 | }; 23 | 24 | } // namespace dcc 25 | -------------------------------------------------------------------------------- /include/dcc/bidi/app/srq.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:srq 6 | /// 7 | /// \file dcc/bidi/app/srq.hpp 8 | /// \author Vincent Hamp 9 | /// \date 03/09/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | #include "../../address.hpp" 15 | 16 | namespace dcc::bidi::app { 17 | 18 | struct Srq { 19 | Address::value_type d{}; 20 | constexpr bool operator==(Srq const&) const = default; 21 | }; 22 | 23 | } // namespace dcc::bidi::app 24 | -------------------------------------------------------------------------------- /include/dcc/bidi/app/adr_low.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:adr_low 6 | /// 7 | /// \file dcc/bidi/app/adr_low.hpp 8 | /// \author Vincent Hamp 9 | /// \date 17/06/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::bidi::app { 16 | 17 | struct AdrLow { 18 | static constexpr uint8_t id{2u}; 19 | uint8_t d{}; 20 | constexpr bool operator==(AdrLow const&) const = default; 21 | }; 22 | 23 | } // namespace dcc::bidi::app 24 | -------------------------------------------------------------------------------- /include/dcc/bidi/app/dyn.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:dyn 6 | /// 7 | /// \file dcc/bidi/app/dyn.hpp 8 | /// \author Vincent Hamp 9 | /// \date 17/06/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::bidi::app { 16 | 17 | struct Dyn { 18 | static constexpr uint8_t id{7u}; 19 | uint8_t d{}; 20 | uint8_t x{}; 21 | constexpr bool operator==(Dyn const&) const = default; 22 | }; 23 | 24 | } // namespace dcc::bidi::app 25 | -------------------------------------------------------------------------------- /include/dcc/bidi/app/adr_high.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:adr_high 6 | /// 7 | /// \file dcc/bidi/app/adr_high.hpp 8 | /// \author Vincent Hamp 9 | /// \date 17/06/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::bidi::app { 16 | 17 | struct AdrHigh { 18 | static constexpr uint8_t id{1u}; 19 | uint8_t d{}; 20 | constexpr bool operator==(AdrHigh const&) const = default; 21 | }; 22 | 23 | } // namespace dcc::bidi::app 24 | -------------------------------------------------------------------------------- /include/dcc/rx/east_west.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// East-west 6 | /// 7 | /// \file dcc/rx/east_west.hpp 8 | /// \author Vincent Hamp 9 | /// \date 29/11/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | 16 | namespace dcc::rx { 17 | 18 | template 19 | concept EastWest = requires(T t, uint16_t addr, std::optional opt_dir) { 20 | { t.eastWestDirection(addr, opt_dir) }; 21 | }; 22 | 23 | } // namespace dcc::rx 24 | -------------------------------------------------------------------------------- /include/dcc/bidi/app/xpom.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:xpom 6 | /// 7 | /// \file dcc/bidi/app/xpom.hpp 8 | /// \author Vincent Hamp 9 | /// \date 17/06/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::bidi::app { 16 | 17 | struct Xpom { 18 | static constexpr std::array ids{8u, 9u, 10u, 11u}; 19 | uint8_t ss{}; 20 | constexpr bool operator==(Xpom const&) const = default; 21 | }; 22 | 23 | } // namespace dcc::bidi::app 24 | -------------------------------------------------------------------------------- /tests/rx/receive.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | TEST_F(RxTest, invalid_bit_resets_internal_state_machine) { 4 | auto state{RandomInterval(0b0'0000u, 0b1'1111u)}; 5 | EXPECT_CALL(_mock, function(_addrs.primary.value, 0b11111u, state)).Times(0); 6 | 7 | // Springe invalid timings into the packet 8 | for (auto timings{dcc::tx::packet2timings( 9 | make_function_group_f4_f0_packet(_addrs.primary, state))}; 10 | auto t : timings) { 11 | _mock.receive(t); 12 | _mock.receive(RandomInterval( 13 | dcc::rx::Timing::Bit0MaxAnalog, std::numeric_limits::max())); 14 | _mock.execute(); 15 | } 16 | 17 | Execute(); 18 | } 19 | -------------------------------------------------------------------------------- /include/dcc/bidi/direction_status_byte.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:dyn direction status byte 6 | /// 7 | /// \file dcc/bidi/direction_status_byte.hpp 8 | /// \author Vincent Hamp 9 | /// \date 15/06/2023 10 | 11 | #pragma once 12 | 13 | #include "app/dyn.hpp" 14 | 15 | namespace dcc::bidi { 16 | 17 | struct DirectionStatusByte : app::Dyn { 18 | explicit constexpr DirectionStatusByte(uint8_t byte) 19 | : app::Dyn{.d = byte, .x = 27u} {} 20 | }; 21 | 22 | } // namespace dcc::bidi 23 | -------------------------------------------------------------------------------- /tests/tx/service.cpp: -------------------------------------------------------------------------------- 1 | #include "tx_test.hpp" 2 | 3 | TEST_F(TxTest, service_idle) { 4 | auto packet{dcc::make_reset_packet()}; 5 | _packet_mock.init(_cfg, packet); 6 | _timings_mock.init(_cfg, packet); 7 | auto timings{dcc::tx::packet2timings(packet, _cfg)}; 8 | auto bits{(_cfg.num_preamble + // Preamble 9 | std::size(packet) * (1uz + CHAR_BIT) // Start + data 10 | + 1uz) * // End 11 | 2uz}; 12 | for (auto i{0uz}; i < bits; ++i) 13 | EXPECT_ALL_EQ(timings[static_cast(i)], 14 | _packet_mock.transmit(), 15 | _timings_mock.transmit()); 16 | } 17 | -------------------------------------------------------------------------------- /tests/instruction.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | TEST(instruction, decode_instruction_short_address) { 5 | auto packet{dcc::make_consist_control_packet(3u, 0u)}; 6 | EXPECT_EQ(dcc::decode_instruction(packet), dcc::Instruction::ConsistControl); 7 | } 8 | 9 | TEST(instruction, decode_instruction_long_address) { 10 | { 11 | auto packet{dcc::make_function_group_f4_f0_packet(1022u, 0u)}; 12 | EXPECT_EQ(dcc::decode_instruction(packet), dcc::Instruction::FunctionGroup); 13 | } 14 | 15 | { 16 | auto packet{dcc::make_consist_control_packet(1337u, 0u)}; 17 | EXPECT_EQ(dcc::decode_instruction(packet), 18 | dcc::Instruction::ConsistControl); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /include/dcc/rx/async_readable.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Asynchronously readable 6 | /// 7 | /// \file dcc/rx/async_readable.hpp 8 | /// \author Vincent Hamp 9 | /// \date 29/11/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | namespace dcc::rx { 18 | 19 | template 20 | concept AsyncReadable = requires( 21 | T t, uint32_t cv_addr, uint8_t byte, std::function cb) { 22 | { t.readCv(cv_addr, byte, cb) }; 23 | }; 24 | 25 | } // namespace dcc::rx 26 | -------------------------------------------------------------------------------- /include/dcc/rx/async_writable.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Asynchronously writable 6 | /// 7 | /// \file dcc/rx/async_writable.hpp 8 | /// \author Vincent Hamp 9 | /// \date 29/11/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | namespace dcc::rx { 18 | 19 | template 20 | concept AsyncWritable = requires( 21 | T t, uint32_t cv_addr, uint8_t byte, std::function cb) { 22 | { t.writeCv(cv_addr, byte, cb) }; 23 | }; 24 | 25 | } // namespace dcc::rx 26 | -------------------------------------------------------------------------------- /include/dcc/bidi/acks.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi ACKs 6 | /// 7 | /// \file dcc/bidi/acks.hpp 8 | /// \author Vincent Hamp 9 | /// \date 15/05/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | 16 | namespace dcc::bidi { 17 | 18 | using Ack = uint8_t; 19 | 20 | /// Instruction understood and will be executed 21 | /// 22 | /// For some stupid, incomprehensible reason, there are two versions of ACK. 23 | inline constexpr std::array acks{0b0000'1111u, 0b1111'0000u}; 24 | 25 | } // namespace dcc::bidi 26 | -------------------------------------------------------------------------------- /include/dcc/bidi/track_voltage.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:dyn track voltage 6 | /// 7 | /// \file dcc/bidi/track_voltage.hpp 8 | /// \author Vincent Hamp 9 | /// \date 15/06/2023 10 | 11 | #pragma once 12 | 13 | #include 14 | #include "app/dyn.hpp" 15 | 16 | namespace dcc::bidi { 17 | 18 | struct TrackVoltage : app::Dyn { 19 | explicit constexpr TrackVoltage(int32_t mV) 20 | : app::Dyn{.d = static_cast(std::max(0, mV - 5000) / 100), 21 | .x = 46u} {} 22 | }; 23 | 24 | } // namespace dcc::bidi 25 | -------------------------------------------------------------------------------- /include/dcc/tx/command_station.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Command station 6 | /// 7 | /// \file dcc/tx/command_station.hpp 8 | /// \author Vincent Hamp 9 | /// \date 15/02/2023 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::tx { 16 | 17 | template 18 | concept CommandStation = requires(T t, bool N, bool P) { 19 | { t.trackOutputs(N, P) }; 20 | { t.packetEnd() }; 21 | { t.biDiStart() }; 22 | { t.biDiChannel1() }; 23 | { t.biDiChannel2() }; 24 | { t.biDiEnd() }; 25 | }; 26 | 27 | } // namespace dcc::tx 28 | -------------------------------------------------------------------------------- /include/dcc/bidi/temperature.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:dyn temperature 6 | /// 7 | /// \file dcc/bidi/temperature.hpp 8 | /// \author Vincent Hamp 9 | /// \date 15/06/2023 10 | 11 | #pragma once 12 | 13 | #include 14 | #include "app/dyn.hpp" 15 | 16 | namespace dcc::bidi { 17 | 18 | struct Temperature : app::Dyn { 19 | explicit constexpr Temperature(int32_t temp) 20 | : app::Dyn{ 21 | .d = static_cast(ztl::lerp(temp, -50, 205, 0, 255)), 22 | .x = 26u} {} 23 | }; 24 | 25 | } // namespace dcc::bidi 26 | -------------------------------------------------------------------------------- /include/dcc/bidi/kmh.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi app:dyn kmh 6 | /// 7 | /// \file dcc/bidi/kmh.hpp 8 | /// \author Vincent Hamp 9 | /// \date 15/06/2023 10 | 11 | #pragma once 12 | 13 | #include "app/dyn.hpp" 14 | 15 | namespace dcc::bidi { 16 | 17 | struct Kmh : app::Dyn { 18 | explicit constexpr Kmh(int32_t kmh) 19 | : Dyn{.d = static_cast(kmh < 512 ? (kmh < 256 ? kmh : kmh - 256) 20 | : 255), 21 | .x = static_cast(kmh < 256 ? 0u : 1u)} {} 22 | }; 23 | 24 | } // namespace dcc::bidi 25 | -------------------------------------------------------------------------------- /include/dcc/rx/writable.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Writable 6 | /// 7 | /// \file dcc/rx/writable.hpp 8 | /// \author Vincent Hamp 9 | /// \date 29/11/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | 16 | namespace dcc::rx { 17 | 18 | template 19 | concept Writable = 20 | requires(T t, uint32_t cv_addr, uint8_t byte, bool bit, uint32_t pos) { 21 | { t.writeCv(cv_addr, byte) } -> std::convertible_to; 22 | { t.writeCv(cv_addr, bit, pos) } -> std::convertible_to; 23 | }; 24 | 25 | } // namespace dcc::rx 26 | -------------------------------------------------------------------------------- /tests/rx/broadcast.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | TEST_F(RxTest, broadcast_speed_and_direction) { 4 | EXPECT_CALL(_mock, direction(0u, _)).Times(0); 5 | EXPECT_CALL(_mock, speed(0u, dcc::scale_speed<28>(17))); 6 | ReceiveAndExecute(dcc::make_speed_and_direction_packet(0u, 1u << 5u | 10u)); 7 | } 8 | 9 | TEST_F(RxTest, broadcast_speed_and_direction_f0_exception) { 10 | _cvs[29uz - 1uz] = 0b1000u; 11 | SetUp(); 12 | 13 | EXPECT_CALL(_mock, function(0u, 1u, 1u)).Times(0); 14 | ReceiveAndExecute(dcc::make_speed_and_direction_packet(0u, 0b01'0000u)); 15 | } 16 | 17 | TEST_F(RxTest, broadcast_speed_and_direction_estop) { 18 | EXPECT_CALL(_mock, direction(0u, _)).Times(0); 19 | EXPECT_CALL(_mock, speed(0u, dcc::EStop)); 20 | ReceiveAndExecute(dcc::make_speed_and_direction_packet(0u, 0b00'0001u)); 21 | } 22 | -------------------------------------------------------------------------------- /include/dcc/rx/readable.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Readable 6 | /// 7 | /// \file dcc/rx/readable.hpp 8 | /// \author Vincent Hamp 9 | /// \date 29/11/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | 16 | namespace dcc::rx { 17 | 18 | template 19 | concept Readable = 20 | requires(T t, uint32_t cv_addr, uint8_t byte, bool bit, uint32_t pos) { 21 | { t.readCv(cv_addr) } -> std::convertible_to; 22 | { t.readCv(cv_addr, byte) } -> std::convertible_to; 23 | { t.readCv(cv_addr, bit, pos) } -> std::convertible_to; 24 | }; 25 | 26 | } // namespace dcc::rx 27 | -------------------------------------------------------------------------------- /include/dcc/tx/config.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Transmit configuration 6 | /// 7 | /// \file dcc/tx/config.hpp 8 | /// \author Vincent Hamp 9 | /// \date 12/06/2023 10 | 11 | #pragma once 12 | 13 | #include "timing.hpp" 14 | 15 | namespace dcc::tx { 16 | 17 | struct Config { 18 | /// Number of preamble bits [17-30] 19 | uint8_t num_preamble{DCC_TX_MIN_PREAMBLE_BITS}; 20 | 21 | /// Duration of 1 bit [52-64] 22 | uint8_t bit1_duration{Bit1}; 23 | 24 | /// Duration of 0 bit [90-119] 25 | uint8_t bit0_duration{Bit0}; 26 | 27 | struct { 28 | /// Enable BiDi 29 | bool bidi{true}; 30 | } flags{}; 31 | }; 32 | 33 | } // namespace dcc::tx 34 | -------------------------------------------------------------------------------- /tests/utility.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define EXPECT_EQ_MACRO(r, first, elem) EXPECT_EQ(first, elem); 7 | 8 | #define EXPECT_ALL_EQ(first, ...) \ 9 | do { \ 10 | BOOST_PP_SEQ_FOR_EACH( \ 11 | EXPECT_EQ_MACRO, first, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) \ 12 | } while (0) 13 | 14 | #define EXPECT_ALL_TRUE(...) \ 15 | do { \ 16 | BOOST_PP_SEQ_FOR_EACH( \ 17 | EXPECT_EQ_MACRO, true, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) \ 18 | } while (0) 19 | -------------------------------------------------------------------------------- /examples/repl/src/fifo.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | // (Very very) minimalistic FIFO with internal locking 7 | template 8 | struct FiFo { 9 | T front() const { return _deque.front(); } 10 | 11 | T back() const { return _deque.back(); } 12 | 13 | void push_back(T value) { 14 | std::scoped_lock lk{_m}; 15 | _deque.push_back(value); 16 | } 17 | 18 | void pop_front() { 19 | std::scoped_lock lk{_m}; 20 | _deque.pop_front(); 21 | } 22 | 23 | bool empty() const { return std::empty(_deque); } 24 | 25 | size_t size() const { return std::size(_deque); } 26 | 27 | private: 28 | std::mutex _m{}; 29 | std::deque _deque; 30 | }; 31 | 32 | template 33 | bool empty(FiFo const& fifo) { 34 | return fifo.empty(); 35 | } 36 | 37 | template 38 | bool size(FiFo const& fifo) { 39 | return fifo.size(); 40 | } 41 | -------------------------------------------------------------------------------- /examples/stm32/src/bsp.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define TIMER_IRQ_HANDLER TIM15_IRQHandler 7 | 8 | #ifdef __cplusplus 9 | extern "C" { 10 | #endif 11 | 12 | // Initialize board to decoder (PE5 and PE6 are track inputs) 13 | void bsp_init_decoder(void); 14 | 15 | // Initialize board to command station (PE5 and PE5 are track outputs) 16 | void bsp_init_command_station(void); 17 | 18 | // Handle decoder interrupt 19 | uint32_t bsp_decoder_irq(void); 20 | 21 | // Handle command station interrupt 22 | void bsp_command_station_irq(uint32_t arr); 23 | 24 | // Set track outputs 25 | void bsp_write_track(bool N, bool P); 26 | 27 | // Write LEDs 28 | void bsp_write_green_led(bool on); 29 | void bsp_write_yellow_led(bool on); 30 | void bsp_write_red_led(bool on); 31 | 32 | // Delay milliseconds 33 | void bsp_delay(uint32_t ms); 34 | 35 | #ifdef __cplusplus 36 | } 37 | #endif -------------------------------------------------------------------------------- /include/dcc/bidi/channel.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi channel type aliases 6 | /// 7 | /// \file dcc/bidi/channel.hpp 8 | /// \author Vincent Hamp 9 | /// \date 12/02/2023 10 | 11 | #pragma once 12 | 13 | #include "datagram.hpp" 14 | 15 | namespace dcc::bidi { 16 | 17 | inline constexpr auto channel1_size{datagram_size}; 18 | using Channel1 = std::array; 19 | 20 | inline constexpr auto channel2_size{datagram_size}; 21 | using Channel2 = std::array; 22 | 23 | inline constexpr auto bundled_channels_size{channel1_size + channel2_size}; 24 | using BundledChannels = std::array; 25 | 26 | } // namespace dcc::bidi 27 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(GoogleTest) 2 | 3 | file(GLOB_RECURSE SRC *.cpp) 4 | add_executable(DCCTests ${SRC}) 5 | 6 | sanitize(address,undefined) 7 | 8 | target_common_warnings(DCCTests PRIVATE) 9 | target_common_errors(DCCTests PRIVATE -Werror) 10 | 11 | cpmaddpackage( 12 | NAME 13 | Boost 14 | VERSION 15 | 1.88.0 16 | URL 17 | https://github.com/boostorg/boost/releases/download/boost-1.88.0/boost-1.88.0-cmake.tar.gz 18 | URL_HASH 19 | SHA256=dcea50f40ba1ecfc448fdf886c0165cf3e525fef2c9e3e080b9804e8117b9694 20 | OPTIONS 21 | "BOOST_ENABLE_CMAKE ON" 22 | "BOOST_SKIP_INSTALL_RULES ON" 23 | "BUILD_SHARED_LIBS OFF" 24 | "BOOST_INCLUDE_LIBRARIES preprocessor") 25 | cpmaddpackage(URI "gh:google/googletest#main" OPTIONS "INSTALL_GTEST OFF") 26 | 27 | target_link_libraries(DCCTests PRIVATE Boost::preprocessor DCC::DCC 28 | GTest::gtest_main GTest::gmock) 29 | 30 | gtest_discover_tests(DCCTests) 31 | -------------------------------------------------------------------------------- /tests/rx/rx_mock.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct RxMock : dcc::rx::CrtpBase { 8 | MOCK_METHOD(void, direction, (uint32_t, int32_t)); 9 | MOCK_METHOD(void, speed, (uint32_t, int32_t)); 10 | MOCK_METHOD(void, function, (uint32_t, uint32_t, uint32_t)); 11 | MOCK_METHOD(void, serviceModeHook, (bool)); 12 | MOCK_METHOD(void, serviceAck, ()); 13 | MOCK_METHOD(uint8_t, readCv, (uint32_t)); 14 | MOCK_METHOD(uint8_t, readCv, (uint32_t, uint8_t)); 15 | MOCK_METHOD(uint8_t, writeCv, (uint32_t, uint8_t)); 16 | MOCK_METHOD(bool, readCv, (uint32_t, bool, uint32_t)); 17 | MOCK_METHOD(bool, writeCv, (uint32_t, bool, uint32_t)); 18 | MOCK_METHOD(void, readCv, (uint32_t, uint8_t, std::function)); 19 | MOCK_METHOD(void, writeCv, (uint32_t, uint8_t, std::function)); 20 | MOCK_METHOD(void, transmitBiDi, (std::span)); 21 | }; 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | arm-none-eabi-gcc: 11 | uses: ZIMO-Elektronik/.github-workflows/.github/workflows/arm-none-eabi-gcc.yml@v0.2.0 12 | with: 13 | arch: -mcpu=cortex-m7 14 | target: DCCStm32Decoder DCCStm32CommandStation 15 | 16 | esp-elf-gcc: 17 | runs-on: ubuntu-24.04 18 | strategy: 19 | matrix: 20 | esp_idf_version: [latest, v5.5.1, v5.4.3, v5.3.4, v5.2.6, v5.1.6] 21 | steps: 22 | - uses: actions/checkout@v5.0.0 23 | with: 24 | fetch-depth: 0 25 | - uses: espressif/esp-idf-ci-action@v1.2.0 26 | with: 27 | path: examples/esp32 28 | esp_idf_version: ${{ matrix.esp_idf_version }} 29 | target: esp32 30 | 31 | x86_64-linux-gnu-gcc: 32 | uses: ZIMO-Elektronik/.github-workflows/.github/workflows/x86_64-linux-gnu-gcc.yml@v0.2.0 33 | -------------------------------------------------------------------------------- /tests/tx/timings_adapter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static_assert(std::is_trivially_copyable_v); 5 | static_assert(std::ranges::range); 6 | static_assert(std::ranges::input_range); 7 | static_assert(std::input_iterator); 8 | 9 | TEST(TimingsAdapter, compare_to_packet2timings) { 10 | { 11 | auto packet{dcc::make_idle_packet()}; 12 | auto timings{dcc::tx::packet2timings(packet)}; 13 | dcc::tx::TimingsAdapter timings_range{packet, dcc::tx::Config{}}; 14 | EXPECT_TRUE(std::ranges::equal(timings, timings_range)); 15 | } 16 | 17 | { 18 | auto packet{ 19 | dcc::make_advanced_operations_speed_direction_and_functions_packet( 20 | 42u, 100u, 10u, 19u)}; 21 | auto timings{dcc::tx::packet2timings(packet)}; 22 | dcc::tx::TimingsAdapter timings_range{packet, dcc::tx::Config{}}; 23 | EXPECT_TRUE(std::ranges::equal(timings, timings_range)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /include/dcc/tx/timing.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Transmit timing 6 | /// 7 | /// \file dcc/tx/timing.hpp 8 | /// \author Vincent Hamp 9 | /// \date 06/02/2024 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::tx { 16 | 17 | enum Timing { 18 | Bit1Min = DCC_TX_MIN_BIT_1_TIMING, ///< Minimal timing for half a 1-bit 19 | Bit1 = 58u, ///< Standard timing for half a 1-bit 20 | Bit1Max = DCC_TX_MAX_BIT_1_TIMING, ///< Maximal timing for half a 1-bit 21 | Bit0Min = DCC_TX_MIN_BIT_0_TIMING, ///< Minimal timing for half a 0-bit 22 | Bit0 = 100u, ///< Standard timing for half a 0-bit 23 | Bit0Max = DCC_TX_MAX_BIT_0_TIMING, ///< Maximal timing for half a 0-bit 24 | Bit0MaxAnalog = 9898u ///< Maximal timing for half a 0-bit analog 25 | }; 26 | 27 | } // namespace dcc::tx 28 | -------------------------------------------------------------------------------- /include/dcc/bidi/timing.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi timing 6 | /// 7 | /// \file dcc/bidi/timing.hpp 8 | /// \author Vincent Hamp 9 | /// \date 06/02/2024 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | namespace dcc::bidi { 16 | 17 | enum Timing { 18 | TCSMin = 26u, ///< Minimal timing for cutout start 19 | TCS = 29u, ///< Standard timing for cutout start 20 | TCSMax = 32u, ///< Maximal timing for cutout start 21 | TCEMin = 454u, ///< Minimal timing for cutout end 22 | TCE = 471u, ///< Standard timing for cutout end 23 | TCEMax = 488u, ///< Maximal timing for cutout end 24 | TTS1 = 80u, ///< Minimal timing for start channel 1 25 | TTC1 = 177u, ///< Maximal timing for end channel 1 26 | TTS2 = 193u, ///< Minimal timing for start channel 2 27 | TTC2 = 454u ///< Maximal timing for end channel 2 28 | }; 29 | 30 | } // namespace dcc::bidi 31 | -------------------------------------------------------------------------------- /examples/repl/src/decoder.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct Decoder : dcc::rx::CrtpBase { 6 | friend dcc::rx::CrtpBase; 7 | 8 | Decoder(); 9 | 10 | private: 11 | // Set direction (1 forward, 0 backward) 12 | void direction(uint16_t addr, bool dir); 13 | 14 | // Set speed [-1, 255] 15 | void speed(uint16_t addr, int32_t speed); 16 | 17 | // Set function inputs 18 | void function(uint16_t addr, uint32_t mask, uint32_t state); 19 | 20 | // Enter or exit service mode 21 | void serviceModeHook(bool service_mode); 22 | 23 | // Generate current pulse as service ACK 24 | void serviceAck(); 25 | 26 | // Transmit BiDi 27 | void transmitBiDi(std::span bytes); 28 | 29 | // Read CV 30 | uint8_t readCv(uint32_t cv_addr, uint8_t byte = 0u); 31 | 32 | // Write CV 33 | uint8_t writeCv(uint32_t cv_addr, uint8_t byte); 34 | 35 | // Read CV bit 36 | bool readCv(uint32_t cv_addr, bool bit, uint32_t pos); 37 | 38 | // Write CV bit 39 | bool writeCv(uint32_t cv_addr, bool bit, uint32_t pos); 40 | 41 | std::array _cvs{}; 42 | }; 43 | -------------------------------------------------------------------------------- /include/dcc/exor.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Ex-or 6 | /// 7 | /// \file dcc/exor.hpp 8 | /// \author Vincent Hamp 9 | /// \date 13/02/2023 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | namespace dcc { 18 | 19 | /// Exclusive disjunction (ex-or) 20 | /// 21 | /// \param bytes Bytes to calculate ex-or for 22 | /// \return Ex-or 23 | constexpr uint8_t exor(std::span bytes) { 24 | return std::accumulate(cbegin(bytes), 25 | cend(bytes), 26 | static_cast(0u), 27 | [](uint8_t a, uint8_t b) { return a ^ b; }); 28 | } 29 | 30 | /// Exclusive disjunction (ex-or) 31 | /// 32 | /// \param packet Packet 33 | /// \return Ex-or 34 | constexpr uint8_t exor(Packet const& packet) { 35 | // Packet checksum is not part of exor 36 | return exor({cbegin(packet), size(packet) - 1uz}); 37 | } 38 | 39 | } // namespace dcc 40 | -------------------------------------------------------------------------------- /include/dcc/rx/backoff.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Backoff logic 6 | /// 7 | /// \file dcc/rx/backoff.hpp 8 | /// \author Vincent Hamp 9 | /// \date 09/08/2023 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace dcc::rx { 19 | 20 | /// Implements O(2^n) backoff logic 21 | struct Backoff { 22 | constexpr operator bool() { 23 | if (_count) { 24 | --_count; 25 | return true; 26 | } else { 27 | _count = randomCount(); 28 | _range = std::min(_range + 1, 3); 29 | return false; 30 | } 31 | } 32 | 33 | /// Don't backoff next time 34 | void now() { 35 | _range = 0; 36 | _count = 0u; 37 | } 38 | 39 | private: 40 | uint8_t randomCount() const { 41 | return static_cast(rand() % (CHAR_BIT << _range)); 42 | } 43 | 44 | int8_t _range{}; 45 | uint8_t _count{}; 46 | }; 47 | 48 | } // namespace dcc::rx 49 | -------------------------------------------------------------------------------- /examples/esp32/README.md: -------------------------------------------------------------------------------- 1 | | Supported Targets | ESP32 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | 2 | | ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | 3 | 4 | # DCC RMT Example 5 | This example shows how to set up a DCC RMT encoder to send packets using the RMT peripheral. After configuring the hardware, an idle packet is sent in an endless loop. 6 | 7 | ## How to Use Example 8 | ### Hardware Required 9 | * A development board with any supported Espressif SOC chip (see `Supported Targets` table above) 10 | * A USB cable for Power supply and programming 11 | 12 | The GPIO number used in this example can be changed according to your board, by the macro `RMT_GPIO_NUM` defined in the [source file](https://github.com/ZIMO-Elektronik/DCC/blob/master/examples/esp32/main/app_main.cpp). 13 | 14 | ### Build and Flash 15 | Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. 16 | 17 | (To exit the serial monitor, type ``Ctrl-]``.) 18 | 19 | See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects. -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug DCCRepl", 6 | "type": "cppdbg", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/build/examples/repl/DCCRepl", 9 | "args": [], 10 | "stopAtEntry": false, 11 | "cwd": "${workspaceFolder}", 12 | "environment": [], 13 | "externalConsole": false, 14 | "MIMode": "gdb", 15 | "setupCommands": [ 16 | { 17 | "description": "Enable pretty-printing for gdb", 18 | "text": "-enable-pretty-printing", 19 | "ignoreFailures": false 20 | } 21 | ] 22 | }, 23 | { 24 | "name": "Debug DCCTests", 25 | "type": "cppdbg", 26 | "request": "launch", 27 | "program": "${workspaceFolder}/build/tests/DCCTests", 28 | "args": [], 29 | "stopAtEntry": false, 30 | "cwd": "${workspaceFolder}", 31 | "environment": [], 32 | "externalConsole": false, 33 | "MIMode": "gdb", 34 | "setupCommands": [ 35 | { 36 | "description": "Enable pretty-printing for gdb", 37 | "text": "-enable-pretty-printing", 38 | "ignoreFailures": false 39 | } 40 | ] 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /tests/rx/backoff.cpp: -------------------------------------------------------------------------------- 1 | #include "backoff_test.hpp" 2 | 3 | TEST_F(BackoffTest, initial_backoff_is_always_false) { 4 | EXPECT_FALSE(static_cast(_backoff)); 5 | } 6 | 7 | TEST_F(BackoffTest, repeated_values_stay_below_max_range) { 8 | EXPECT_LT(CountTillFalse(CHAR_BIT), CHAR_BIT); 9 | EXPECT_LT(CountTillFalse(CHAR_BIT << 1), CHAR_BIT << 1); 10 | EXPECT_LT(CountTillFalse(CHAR_BIT << 2), CHAR_BIT << 2); 11 | EXPECT_LT(CountTillFalse(CHAR_BIT << 3), CHAR_BIT << 3); 12 | 13 | // Range can't get bigger than CHAR_BIT << 3 (64) 14 | EXPECT_LT(CountTillFalse(CHAR_BIT << 4), CHAR_BIT << 3); 15 | 16 | // Range can be reset 17 | _backoff = {}; 18 | EXPECT_LT(CountTillFalse(CHAR_BIT), CHAR_BIT); 19 | } 20 | 21 | TEST_F(BackoffTest, now) { 22 | EXPECT_LT(CountTillFalse(CHAR_BIT), CHAR_BIT); 23 | EXPECT_LT(CountTillFalse(CHAR_BIT << 1), CHAR_BIT << 1); 24 | EXPECT_LT(CountTillFalse(CHAR_BIT << 2), CHAR_BIT << 2); 25 | EXPECT_LT(CountTillFalse(CHAR_BIT << 3), CHAR_BIT << 3); 26 | 27 | // Range can't get bigger than CHAR_BIT << 3 (64) 28 | EXPECT_LT(CountTillFalse(CHAR_BIT << 4), CHAR_BIT << 3); 29 | 30 | // Range can be reset with now 31 | _backoff.now(); 32 | EXPECT_FALSE(static_cast(_backoff)); 33 | EXPECT_LT(CountTillFalse(CHAR_BIT), CHAR_BIT); 34 | } 35 | -------------------------------------------------------------------------------- /tests/rx/feature_expansion.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | TEST_F(RxTest, feature_expansion_f20_f13) { 4 | auto state{RandomInterval(0x00u, 0xFFu)}; 5 | EXPECT_CALL(_mock, 6 | function(_addrs.primary.value, 7 | 0xFFu << 13u, 8 | static_cast(state << 13u))); 9 | ReceiveAndExecute( 10 | make_feature_expansion_f20_f13_packet(_addrs.primary, state)); 11 | } 12 | 13 | TEST_F(RxTest, feature_expansion_f20_f13_wrong_packet_length) { 14 | auto state{RandomInterval(0x00u, 0xFFu)}; 15 | EXPECT_CALL(_mock, 16 | function(_addrs.primary.value, 17 | 0xFFu << 13u, 18 | static_cast(state << 13u))) 19 | .Times(0); 20 | ReceiveAndExecute(TinkerWithPacketLength( 21 | make_feature_expansion_f20_f13_packet(_addrs.primary, state))); 22 | } 23 | 24 | TEST_F(RxTest, feature_expansion_f28_f21) { 25 | auto state{RandomInterval(0x00u, 0xFFu)}; 26 | EXPECT_CALL(_mock, 27 | function(_addrs.primary.value, 28 | 0xFFu << 21u, 29 | static_cast(state << 21u))); 30 | ReceiveAndExecute( 31 | make_feature_expansion_f28_f21_packet(_addrs.primary, state)); 32 | } 33 | -------------------------------------------------------------------------------- /tests/crc8.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | TEST(crc8, crc8) { 8 | std::vector data{0x0Bu, 9 | 0x0Au, 10 | 0x00u, 11 | 0x00u, 12 | 0x8Eu, 13 | 0x40u, 14 | 0x00u, 15 | 0x0Du, 16 | 0x67u, 17 | 0x00u, 18 | 0x01u, 19 | 0x00u}; 20 | EXPECT_EQ(dcc::crc8(data), 0x4Cu); 21 | 22 | dcc::Packet packet{0x0Bu, 23 | 0x0Au, 24 | 0x00u, 25 | 0x00u, 26 | 0x8Eu, 27 | 0x40u, 28 | 0x00u, 29 | 0x0Du, 30 | 0x67u, 31 | 0x00u, 32 | 0x01u, 33 | 0x00u, 34 | 0xFFu}; // Packet checksum is not part of CRC 35 | EXPECT_EQ(dcc::crc8(packet), 0x4Cu); 36 | 37 | uint8_t crc{}; 38 | for (auto b : data) crc = dcc::crc8(b ^ crc); 39 | EXPECT_EQ(crc, 0x4Cu); 40 | } 41 | -------------------------------------------------------------------------------- /tests/rx/scale_speed.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | TEST(receive_scale_speed, estop) { 5 | EXPECT_EQ(dcc::scale_speed<14>(dcc::EStop), dcc::EStop); 6 | EXPECT_EQ(dcc::scale_speed<28>(dcc::EStop), dcc::EStop); 7 | EXPECT_EQ(dcc::scale_speed<126>(dcc::EStop), dcc::EStop); 8 | } 9 | 10 | TEST(receive_scale_speed, stop) { 11 | EXPECT_EQ(dcc::scale_speed<14>(dcc::Stop), dcc::Stop); 12 | EXPECT_EQ(dcc::scale_speed<28>(dcc::Stop), dcc::Stop); 13 | EXPECT_EQ(dcc::scale_speed<126>(dcc::Stop), dcc::Stop); 14 | } 15 | 16 | TEST(receive_scale_speed, one) { 17 | EXPECT_EQ(dcc::scale_speed<14>(1), dcc::scale_speed<126>(1)); 18 | EXPECT_EQ(dcc::scale_speed<28>(1), dcc::scale_speed<126>(1)); 19 | } 20 | 21 | TEST(receive_scale_speed, two) { 22 | EXPECT_EQ(dcc::scale_speed<14>(2), 21); 23 | EXPECT_EQ(dcc::scale_speed<28>(2), 11); 24 | EXPECT_EQ(dcc::scale_speed<126>(2), 4); 25 | } 26 | 27 | TEST(receive_scale_speed, three) { 28 | EXPECT_EQ(dcc::scale_speed<14>(3), 40); 29 | EXPECT_EQ(dcc::scale_speed<28>(3), 20); 30 | EXPECT_EQ(dcc::scale_speed<126>(3), 6); 31 | } 32 | 33 | TEST(receive_scale_speed, max) { 34 | EXPECT_EQ(dcc::scale_speed<14>(14), 255); 35 | EXPECT_EQ(dcc::scale_speed<28>(28), 255); 36 | EXPECT_EQ(dcc::scale_speed<126>(126), 255); 37 | } 38 | -------------------------------------------------------------------------------- /tests/rx/speed_direction.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | TEST_F(RxTest, speed_and_direction) { 4 | EXPECT_CALL(_mock, direction(_addrs.primary.value, dcc::Forward)); 5 | EXPECT_CALL(_mock, speed(_addrs.primary.value, dcc::scale_speed<28>(17))); 6 | ReceiveAndExecute( 7 | make_speed_and_direction_packet(_addrs.primary, 1u << 5u | 10u)); 8 | } 9 | 10 | TEST_F(RxTest, speed_and_direction_f0_exception) { 11 | _cvs[29uz - 1uz] = 0b1000u; 12 | SetUp(); 13 | 14 | EXPECT_CALL(_mock, function(_addrs.primary.value, 1u, 1u)); 15 | ReceiveAndExecute( 16 | make_speed_and_direction_packet(_addrs.primary, 0b01'0000u)); 17 | } 18 | 19 | TEST_F(RxTest, speed_and_direction_estop) { 20 | EXPECT_CALL(_mock, direction(_addrs.primary.value, dcc::Forward)); 21 | EXPECT_CALL(_mock, speed(_addrs.primary.value, dcc::EStop)); 22 | ReceiveAndExecute( 23 | make_speed_and_direction_packet(_addrs.primary, 1u << 5u | 0b0'0001u)); 24 | } 25 | 26 | TEST_F(RxTest, speed_and_direction_wrong_packet_length) { 27 | EXPECT_CALL(_mock, direction(_addrs.primary.value, dcc::Forward)).Times(0); 28 | EXPECT_CALL(_mock, speed(_addrs.primary.value, dcc::scale_speed<28>(17))) 29 | .Times(0); 30 | ReceiveAndExecute(TinkerWithPacketLength( 31 | make_speed_and_direction_packet(_addrs.primary, 1u << 5u | 10u))); 32 | } 33 | -------------------------------------------------------------------------------- /include/dcc/rx/decoder.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Decoder 6 | /// 7 | /// \file dcc/rx/decoder.hpp 8 | /// \author Vincent Hamp 9 | /// \date 29/11/2022 10 | 11 | #pragma once 12 | 13 | #include "../address.hpp" 14 | #include "readable.hpp" 15 | #include "writable.hpp" 16 | 17 | namespace dcc::rx { 18 | 19 | template 20 | concept Decoder = Readable && Writable && 21 | requires(T t, 22 | Address::value_type addr, 23 | bool dir, 24 | int32_t speed, 25 | uint32_t mask, 26 | uint32_t state, 27 | bool service_mode, 28 | std::span bytes) { 29 | { t.direction(addr, dir) }; 30 | { t.speed(addr, speed) }; 31 | { t.function(addr, mask, state) }; 32 | { t.serviceModeHook(service_mode) }; 33 | { t.serviceAck() }; 34 | { t.transmitBiDi(bytes) }; 35 | }; 36 | 37 | } // namespace dcc::rx 38 | -------------------------------------------------------------------------------- /tests/rx/cv_short.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | TEST_F(RxTest, cv_short_cv23) { 4 | auto cv23{RandomInterval(0u, 255u)}; 5 | auto packet{ 6 | dcc::make_cv_access_short_write_packet(_addrs.primary, 0b0010u, cv23)}; 7 | 8 | EXPECT_CALL(_mock, 9 | writeCv(Matcher(23u - 1u), 10 | Matcher(cv23), 11 | Matcher>(_))) 12 | .WillOnce(InvokeArgument<2uz>(cv23)); 13 | if constexpr (DCC_STANDARD_COMPLIANCE) ReceiveAndExecute(packet); 14 | else ReceiveAndExecuteTwice(packet); 15 | } 16 | 17 | TEST_F(RxTest, cv_short_cv31_32) { 18 | uint8_t cv31{145u}; 19 | uint8_t cv32{0u}; 20 | auto packet{dcc::make_cv_access_short_write_packet( 21 | _addrs.primary, 0b0101u, cv31, cv32)}; 22 | 23 | EXPECT_CALL(_mock, 24 | writeCv(Matcher(31u - 1u), 25 | Matcher(cv31), 26 | Matcher>(_))) 27 | .WillOnce(InvokeArgument<2uz>(cv31)); 28 | EXPECT_CALL(_mock, 29 | writeCv(Matcher(32u - 1u), 30 | Matcher(cv32), 31 | Matcher>(_))) 32 | .WillOnce(InvokeArgument<2uz>(cv32)); 33 | ReceiveAndExecuteTwice(packet); 34 | } 35 | -------------------------------------------------------------------------------- /examples/stm32/src/decoder.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct Decoder : dcc::rx::CrtpBase { 6 | friend dcc::rx::CrtpBase; 7 | 8 | private: 9 | // Set direction (1 forward, 0 backward) 10 | void direction(uint16_t addr, bool dir); 11 | 12 | // Set speed [-1, 255] (regardless of CV settings) 13 | void speed(uint16_t addr, int32_t speed); 14 | 15 | // Set function inputs 16 | void function(uint16_t addr, uint32_t mask, uint32_t state); 17 | 18 | // Enter or exit service mode 19 | void serviceModeHook(bool service_mode); 20 | 21 | // Generate current pulse as service ACK 22 | void serviceAck(); 23 | 24 | // Transmit BiDi 25 | void transmitBiDi(std::span bytes); 26 | 27 | // Read CV 28 | uint8_t readCv(uint32_t cv_addr, uint8_t byte = 0u); 29 | 30 | // Write CV 31 | uint8_t writeCv(uint32_t cv_addr, uint8_t byte); 32 | 33 | // Read CV bit 34 | bool readCv(uint32_t cv_addr, bool, uint32_t pos); 35 | 36 | // Write CV bit 37 | bool writeCv(uint32_t cv_addr, bool bit, uint32_t pos); 38 | 39 | // Minimal set of CVs 40 | std::array _cvs{ 41 | 3u, 1u, 2u, 1u, 1u, 1u, 4u, DCC_MANUFACTURER_ID, 42 | 55u, 0u, 0u, 117u, 128u, 195u, 0u, 0u, 43 | 192u, 128u, 0u, 0u, 0u, 0u, 0u, 0u, 44 | 0u, 0u, 0u, 131u, 14u}; 45 | }; 46 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Run act arm-none-eabi-gcc", 6 | "type": "shell", 7 | "isBackground": true, 8 | "command": "act -j arm-none-eabi-gcc --input arch=-mcpu=cortex-m7 --input target=\"DCCStm32Decoder DCCStm32CommandStation\"" 9 | }, 10 | { 11 | "label": "Run act esp-elf-gcc", 12 | "type": "shell", 13 | "isBackground": true, 14 | "command": "act -j esp-elf-gcc --input path=examples/esp32 --input target=esp32" 15 | }, 16 | { 17 | "label": "Run act tests", 18 | "type": "shell", 19 | "isBackground": true, 20 | "command": "act -j tests --input args=-DCMAKE_BUILD_TYPE=Debug --input target=DCCTests --input post-build=\"ctest --test-dir build --schedule-random --timeout 86400\"" 21 | }, 22 | { 23 | "label": "Run act x86_64-linux-gnu-gcc", 24 | "type": "shell", 25 | "isBackground": true, 26 | "command": "act -j x86_64-linux-gnu-gcc" 27 | }, 28 | { 29 | "label": "Run DCCTests", 30 | "type": "shell", 31 | "isBackground": true, 32 | "command": "./build/tests/DCCTests" 33 | }, 34 | { 35 | "label": "Run PlantUML", 36 | "type": "shell", 37 | "isBackground": true, 38 | "command": "plantuml -o ./images ./data/transmission.pu" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /include/dcc/rx/timing.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Receive timing 6 | /// 7 | /// \file dcc/rx/timing.hpp 8 | /// \author Vincent Hamp 9 | /// \date 06/02/2024 10 | 11 | #pragma once 12 | 13 | #include 14 | #include "../bit.hpp" 15 | 16 | namespace dcc::rx { 17 | 18 | enum Timing { 19 | Bit1Min = DCC_RX_MIN_BIT_1_TIMING, ///< Minimal timing for half a 1-bit 20 | Bit1 = 58u, ///< Standard timing for half a 1-bit 21 | Bit1Max = DCC_RX_MAX_BIT_1_TIMING, ///< Maximal timing for half a 1-bit 22 | Bit0Min = DCC_RX_MIN_BIT_0_TIMING, ///< Minimal timing for half a 0-bit 23 | Bit0 = 100u, ///< Standard timing for half a 0-bit 24 | Bit0Max = DCC_RX_MAX_BIT_0_TIMING, ///< Maximal timing for half a 0-bit 25 | Bit0MaxAnalog = 10000u ///< Maximal timing for half a 0-bit analog 26 | }; 27 | 28 | /// Convert time to bit 29 | /// 30 | /// \param time Time in µs 31 | /// \return Bit 32 | constexpr Bit time2bit(uint32_t time) { 33 | if (time >= Bit1Min && time <= Bit1Max) return _1; 34 | else if (time >= Bit0Min && time <= Bit0MaxAnalog) return _0; 35 | else return Invalid; 36 | } 37 | 38 | } // namespace dcc::rx 39 | -------------------------------------------------------------------------------- /tests/rx/function_group.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | TEST_F(RxTest, function_group_f4_f0) { 4 | auto state{RandomInterval(0b0'0000u, 0b1'1111u)}; 5 | EXPECT_CALL(_mock, function(_addrs.primary.value, 0b11111u, state)); 6 | ReceiveAndExecute(make_function_group_f4_f0_packet(_addrs.primary, state)); 7 | } 8 | 9 | TEST_F(RxTest, function_group_f4_f0_wrong_packet_length) { 10 | auto state{RandomInterval(0b0'0000u, 0b1'1111u)}; 11 | EXPECT_CALL(_mock, function(_addrs.primary.value, 0b11111u, state)).Times(0); 12 | ReceiveAndExecute(TinkerWithPacketLength( 13 | make_function_group_f4_f0_packet(_addrs.primary, state))); 14 | } 15 | 16 | // Ignore F0 if CV29:2 is 0 17 | TEST_F(RxTest, function_group_f4_f0_exception) { 18 | _cvs[29uz - 1uz] = 0b1000u; 19 | SetUp(); 20 | EXPECT_CALL(_mock, function(_addrs.primary.value, 0b11110u, 0b1u)); 21 | ReceiveAndExecute(make_function_group_f4_f0_packet(_addrs.primary, 0b1u)); 22 | } 23 | 24 | TEST_F(RxTest, function_group_f8_f5) { 25 | auto state{RandomInterval(0x0u, 0xFu)}; 26 | EXPECT_CALL(_mock, 27 | function(_addrs.primary.value, 28 | 0xFu << 5u, 29 | static_cast(state << 5u))); 30 | ReceiveAndExecute(make_function_group_f8_f5_packet(_addrs.primary, state)); 31 | } 32 | 33 | TEST_F(RxTest, function_group_f12_f9) { 34 | auto state{RandomInterval(0x0u, 0xFu)}; 35 | EXPECT_CALL(_mock, 36 | function(_addrs.primary.value, 37 | 0xFu << 9u, 38 | static_cast(state << 9u))); 39 | ReceiveAndExecute(make_function_group_f12_f9_packet(_addrs.primary, state)); 40 | } 41 | -------------------------------------------------------------------------------- /tests/rx/consist_control.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | TEST_F(RxTest, consist_control) { 4 | BASIC_ADDRESS_EXPECT_CALL_READ_CV_INIT_SEQUENCE(); 5 | auto cv19{RandomInterval(0u, 255u)}; 6 | auto packet{make_consist_control_packet(_addrs.primary, cv19)}; 7 | 8 | EXPECT_CALL(_mock, 9 | writeCv(Matcher(19u - 1u), 10 | Matcher(cv19), 11 | Matcher>(_))) 12 | .WillOnce(InvokeArgument<2uz>(cv19)); 13 | if constexpr (DCC_STANDARD_COMPLIANCE) ReceiveAndExecute(packet); 14 | else ReceiveAndExecuteTwice(packet); 15 | } 16 | 17 | TEST_F(RxTest, consist_control_wrong_packet_length) { 18 | auto cv19{RandomInterval(0u, 255u)}; 19 | auto packet{ 20 | TinkerWithPacketLength(make_consist_control_packet(_addrs.primary, cv19))}; 21 | 22 | EXPECT_CALL(_mock, 23 | writeCv(Matcher(19u - 1u), 24 | Matcher(cv19), 25 | Matcher>(_))) 26 | .Times(0); 27 | if constexpr (DCC_STANDARD_COMPLIANCE) ReceiveAndExecute(packet); 28 | else ReceiveAndExecuteTwice(packet); 29 | } 30 | 31 | // https://github.com/ZIMO-Elektronik/DCC/issues/82 32 | TEST_F(RxTest, consist_control_wrong_encoding) { 33 | dcc::Packet packet{static_cast(_addrs.primary), 0x16u, 0x15u, 0x00u}; 34 | 35 | EXPECT_CALL(_mock, 36 | writeCv(Matcher(19u - 1u), 37 | Matcher(_), 38 | Matcher>(_))) 39 | .Times(0); 40 | if constexpr (DCC_STANDARD_COMPLIANCE) ReceiveAndExecute(packet); 41 | else ReceiveAndExecuteTwice(packet); 42 | } 43 | -------------------------------------------------------------------------------- /examples/stm32/src/decoder.cpp: -------------------------------------------------------------------------------- 1 | #include "decoder.hpp" 2 | #include 3 | #include 4 | #include "bsp.h" 5 | 6 | void Decoder::direction(uint16_t addr, bool dir) {} 7 | 8 | void Decoder::speed(uint16_t addr, int32_t speed) { 9 | if (speed) { 10 | printf("\nDecoder: accelerate to speed step %d\n", speed); 11 | bsp_write_green_led(true); 12 | } else { 13 | printf("Decoder: stop\n"); 14 | bsp_write_green_led(false); 15 | } 16 | } 17 | 18 | void Decoder::function(uint16_t addr, uint32_t mask, uint32_t state) { 19 | if (!(mask & 0b0'1000u)) return; 20 | else if (state & 0b0'1000u) { 21 | printf("Decoder: set function F3\n"); 22 | bsp_write_yellow_led(true); 23 | } else { 24 | printf("Decoder: clear function F3\n"); 25 | bsp_write_yellow_led(false); 26 | } 27 | } 28 | 29 | void Decoder::serviceModeHook(bool service_mode) {} 30 | 31 | void Decoder::serviceAck() {} 32 | 33 | void Decoder::transmitBiDi(std::span bytes) {} 34 | 35 | uint8_t Decoder::readCv(uint32_t cv_addr, uint8_t) { 36 | if (cv_addr >= size(_cvs)) return 0u; 37 | return _cvs[cv_addr]; 38 | } 39 | 40 | uint8_t Decoder::writeCv(uint32_t cv_addr, uint8_t byte) { 41 | if (cv_addr >= size(_cvs)) return 0u; 42 | return _cvs[cv_addr] = byte; 43 | } 44 | 45 | bool Decoder::readCv(uint32_t cv_addr, bool, uint32_t pos) { return false; } 46 | 47 | bool Decoder::writeCv(uint32_t cv_addr, bool bit, uint32_t pos) { 48 | return false; 49 | } 50 | 51 | Decoder decoder; 52 | 53 | extern "C" void TIMER_IRQ_HANDLER() { 54 | auto const ccr{bsp_decoder_irq()}; 55 | decoder.receive(ccr); 56 | } 57 | 58 | int main() { 59 | bsp_init_decoder(); 60 | decoder.init(); 61 | 62 | printf("\n\nBoot\n"); 63 | for (;;) { 64 | decoder.execute(); 65 | bsp_delay(5u); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/rx/decoder_lock.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | TEST_F(RxTest, cv15_not_equal_cv15_activates_decoder_lock) { 4 | _cvs[15uz - 1uz] = 1u; 5 | _cvs[16uz - 1uz] = 2u; 6 | SetUp(); 7 | 8 | auto cv_addr{RandomInterval(30u, smath::pow(2u, 10u) - 1u)}; 9 | auto byte{RandomInterval(0u, 255u)}; 10 | EXPECT_CALL(_mock, 11 | writeCv(Matcher(cv_addr), 12 | Matcher(byte), 13 | Matcher>(_))) 14 | .Times(0); 15 | ReceiveAndExecuteTwice( 16 | dcc::make_cv_access_long_write_packet(_addrs.primary, cv_addr, byte)); 17 | } 18 | 19 | TEST_F(RxTest, cv15_zero_deactivates_decoder_lock) { 20 | _cvs[15uz - 1uz] = 0u; 21 | _cvs[16uz - 1uz] = 1u; 22 | SetUp(); 23 | 24 | auto cv_addr{RandomInterval(30u, smath::pow(2u, 10u) - 1u)}; 25 | auto byte{RandomInterval(0u, 255u)}; 26 | EXPECT_CALL(_mock, 27 | writeCv(Matcher(cv_addr), 28 | Matcher(byte), 29 | Matcher>(_))) 30 | .WillOnce(InvokeArgument<2uz>(byte)); 31 | ReceiveAndExecuteTwice( 32 | dcc::make_cv_access_long_write_packet(_addrs.primary, cv_addr, byte)); 33 | } 34 | 35 | TEST_F(RxTest, cv16_zero_deactivates_decoder_lock) { 36 | _cvs[15uz - 1uz] = 42u; 37 | _cvs[16uz - 1uz] = 0u; 38 | SetUp(); 39 | 40 | auto cv_addr{RandomInterval(30u, smath::pow(2u, 10u) - 1u)}; 41 | auto byte{RandomInterval(0u, 255u)}; 42 | EXPECT_CALL(_mock, 43 | writeCv(Matcher(cv_addr), 44 | Matcher(byte), 45 | Matcher>(_))) 46 | .WillOnce(InvokeArgument<2uz>(byte)); 47 | ReceiveAndExecuteTwice( 48 | dcc::make_cv_access_long_write_packet(_addrs.primary, cv_addr, byte)); 49 | } 50 | -------------------------------------------------------------------------------- /tests/tx/tx_test.cpp: -------------------------------------------------------------------------------- 1 | #include "tx_test.hpp" 2 | 3 | TxTest::TxTest() {} 4 | 5 | TxTest::~TxTest() {} 6 | 7 | void TxTest::SetUp() { 8 | _packet_mock.init(_cfg); 9 | _timings_mock.init(_cfg); 10 | 11 | auto packet{dcc::make_idle_packet()}; 12 | 13 | auto timings{dcc::tx::packet2timings(packet, _cfg)}; 14 | 15 | auto bits{(_cfg.num_preamble + // Preamble 16 | std::size(packet) * (1uz + CHAR_BIT) // Start + data 17 | + 1uz) * // End 18 | 2uz}; 19 | 20 | // Idle packet 21 | for (auto i{0uz}; i < bits; ++i) 22 | EXPECT_ALL_EQ(timings[static_cast(i)], 23 | _packet_mock.transmit(), 24 | _timings_mock.transmit()); 25 | 26 | // BiDi 27 | if (_cfg.flags.bidi) { 28 | EXPECT_ALL_EQ( 29 | static_cast(dcc::bidi::Timing::TCS), 30 | _packet_mock.transmit(), 31 | _timings_mock.transmit()); 32 | EXPECT_ALL_EQ(static_cast( 33 | dcc::bidi::Timing::TTS1 - dcc::bidi::Timing::TCS), 34 | _packet_mock.transmit(), 35 | _timings_mock.transmit()); 36 | EXPECT_ALL_EQ(static_cast( 37 | dcc::bidi::Timing::TTS2 - dcc::bidi::Timing::TTS1), 38 | _packet_mock.transmit(), 39 | _timings_mock.transmit()); 40 | EXPECT_ALL_EQ(static_cast( 41 | dcc::bidi::Timing::TTC2 - dcc::bidi::Timing::TTS2), 42 | _packet_mock.transmit(), 43 | _timings_mock.transmit()); 44 | EXPECT_ALL_EQ(static_cast( 45 | dcc::bidi::Timing::TCE - dcc::bidi::Timing::TTC2), 46 | _packet_mock.transmit(), 47 | _timings_mock.transmit()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/stm32/src/command_station.cpp: -------------------------------------------------------------------------------- 1 | #include "command_station.hpp" 2 | #include 3 | #include "bsp.h" 4 | 5 | void CommandStation::trackOutputs(bool N, bool P) { bsp_write_track(N, P); } 6 | 7 | CommandStation command_station; 8 | 9 | extern "C" void TIMER_IRQ_HANDLER() { 10 | auto const arr{command_station.transmit()}; 11 | bsp_command_station_irq(arr); 12 | } 13 | 14 | int main() { 15 | bsp_init_command_station(); 16 | command_station.init({.num_preamble = DCC_TX_MIN_PREAMBLE_BITS, 17 | .bit1_duration = 58u, 18 | .bit0_duration = 100u, 19 | .flags = {.bidi = true}}); 20 | 21 | // Turn red LED on to indicate this board is the command station 22 | bsp_write_red_led(true); 23 | 24 | printf("\n\nBoot\n"); 25 | bsp_delay(2000u); 26 | dcc::Packet packet{}; 27 | for (;;) { 28 | // Accelerate 29 | packet = 30 | dcc::make_advanced_operations_speed_packet(3u, dcc::Forward << 7u | 42u); 31 | command_station.packet(packet); 32 | printf("\nCommand station: accelerate to speed step 42\n"); 33 | bsp_write_green_led(true); 34 | bsp_delay(2000u); 35 | 36 | // Set function F3 37 | packet = dcc::make_function_group_f4_f0_packet(3u, 0b0'1000u); 38 | command_station.packet(packet); 39 | printf("Command station: set function F3\n"); 40 | bsp_write_yellow_led(true); 41 | bsp_delay(2000u); 42 | 43 | // Decelerate 44 | packet = 45 | dcc::make_advanced_operations_speed_packet(3u, dcc::Forward << 7u | 0u); 46 | command_station.packet(packet); 47 | printf("Command station: stop\n"); 48 | bsp_write_green_led(false); 49 | bsp_delay(2000u); 50 | 51 | // Clear function 52 | packet = dcc::make_function_group_f4_f0_packet(3u, 0b0'0000u); 53 | command_station.packet(packet); 54 | printf("Command station: clear function F3\n"); 55 | bsp_write_yellow_led(false); 56 | bsp_delay(2000u); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/esp32/main/app_main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define RMT_GPIO_NUM GPIO_NUM_21 9 | 10 | extern "C" void app_main() { 11 | printf("DCC RMT encoder example"); 12 | 13 | // Setup RMT on RMT_GPIO_NUM 14 | rmt_tx_channel_config_t chan_config{.gpio_num = RMT_GPIO_NUM, 15 | .clk_src = RMT_CLK_SRC_DEFAULT, 16 | .resolution_hz = 1'000'000u, // 1MHz 17 | .mem_block_symbols = 18 | SOC_RMT_CHANNELS_PER_GROUP * 19 | SOC_RMT_MEM_WORDS_PER_CHANNEL, 20 | .trans_queue_depth = 2uz, 21 | .intr_priority = 0, 22 | .flags = {}}; 23 | rmt_channel_handle_t rmt_channel{}; 24 | ESP_ERROR_CHECK(rmt_new_tx_channel(&chan_config, &rmt_channel)); 25 | ESP_ERROR_CHECK(rmt_enable(rmt_channel)); 26 | 27 | // New DCC encoder 28 | dcc_encoder_config_t encoder_config{.num_preamble = DCC_TX_MIN_PREAMBLE_BITS, 29 | .bidibit_duration = 60u, 30 | .bit1_duration = 58u, 31 | .bit0_duration = 100u, 32 | .endbit_duration = 58u - 24u, 33 | .flags{.level0 = false, .zimo0 = true}}; 34 | rmt_encoder_handle_t rmt_encoder{}; 35 | ESP_ERROR_CHECK(rmt_new_dcc_encoder(&encoder_config, &rmt_encoder)); 36 | 37 | auto idle_packet{dcc::make_idle_packet()}; 38 | rmt_transmit_config_t rmt_transmit_config{}; 39 | for (;;) 40 | ESP_ERROR_CHECK(rmt_transmit(rmt_channel, 41 | rmt_encoder, 42 | data(idle_packet), 43 | size(idle_packet), 44 | &rmt_transmit_config)); 45 | } 46 | -------------------------------------------------------------------------------- /tests/bidi/encode_decode.cpp: -------------------------------------------------------------------------------- 1 | #include "encode_decode_test.hpp" 2 | 3 | #define DATA0 1u, 1u 4 | #define DATA0_NOID 1u << 8u | 1u 5 | 6 | // First databyte max 63 7 | #define DATA1 2u, 63u << 8u | 42u 8 | #define DATA1_NOID 2u << 14u | 63u << 8u | 42u 9 | 10 | // First databyte max 15 11 | #define DATA2 9u, 15u << 16u | 122u << 8u | 39u 12 | #define DATA2_NOID 9u << 20u | 15u << 16u | 122u << 8u | 39u 13 | 14 | #define DATA3 13u, 219u << 24u | 190u << 16u | 74u << 8u | 11u 15 | #define DATA3_NOID \ 16 | static_cast(13u) << 32u | 219u << 24u | 190u << 16u | 74u << 8u | \ 17 | 11u 18 | 19 | // Encoding and decoding some BiDi messages. All data must be equal 20 | // afterwards. 21 | TEST_F(EncodeDecodeTest, encode_decode) { 22 | auto e0{encode_datagram(make_datagram(DATA0))}; 23 | auto e1{encode_datagram(make_datagram(DATA1))}; 24 | auto e2{encode_datagram(make_datagram(DATA2))}; 25 | auto e3{encode_datagram(make_datagram(DATA3))}; 26 | 27 | auto d0{make_data(decode_datagram(e0))}; 28 | auto d1{make_data(decode_datagram(e1))}; 29 | auto d2{make_data(decode_datagram(e2))}; 30 | auto d3{make_data(decode_datagram(e3))}; 31 | 32 | EXPECT_EQ(d0, DATA0_NOID); 33 | EXPECT_EQ(d1, DATA1_NOID); 34 | EXPECT_EQ(d2, DATA2_NOID); 35 | EXPECT_EQ(d3, DATA3_NOID); 36 | } 37 | 38 | TEST_F(EncodeDecodeTest, too_many_bits) { 39 | // 2 byte message does not fit 18bit, assertion triggered 40 | ASSERT_DEBUG_DEATH(make_datagram(0u, 64u << 8u), ".*"); 41 | 42 | // 3 byte message does not fit 24bit, assertion triggered 43 | ASSERT_DEBUG_DEATH(make_datagram(0u, 16u << 24u), ".*"); 44 | } 45 | 46 | // make_datagram has 2 overloads, one which takes a seperate ID and one wich 47 | // doesn't. Both should produce the same datagram however. 48 | TEST_F(EncodeDecodeTest, compare_datagram_with_and_without_ids) { 49 | EXPECT_EQ(make_datagram(DATA0), 50 | make_datagram(DATA0_NOID)); 51 | EXPECT_EQ(make_datagram(DATA1), 52 | make_datagram(DATA1_NOID)); 53 | EXPECT_EQ(make_datagram(DATA2), 54 | make_datagram(DATA2_NOID)); 55 | EXPECT_EQ(make_datagram(DATA3), 56 | make_datagram(DATA3_NOID)); 57 | } 58 | -------------------------------------------------------------------------------- /examples/stm32/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cpmaddpackage("gh:STMicroelectronics/STM32CubeH7@1.12.1") 2 | 3 | function(add_stm32_target TARGET) 4 | file(GLOB_RECURSE SRC 5 | ${STM32CubeH7_SOURCE_DIR}/Drivers/STM32H7xx_HAL_Driver/*.c 6 | src/startup_stm32h743zitx.s src/bsp.c) 7 | # Filter unwanted .template files 8 | list(FILTER SRC EXCLUDE REGEX ".*template") 9 | add_executable(${TARGET} ${SRC}) 10 | 11 | # Include either decoder.cpp or command_station.cpp, but not both 12 | target_sources( 13 | ${TARGET} 14 | PRIVATE 15 | "$,src/decoder.cpp,src/command_station.cpp>" 16 | ) 17 | 18 | target_compile_definitions(${TARGET} PUBLIC USE_FULL_LL_DRIVER USE_HAL_DRIVER 19 | STM32H743xx) 20 | 21 | target_include_directories( 22 | ${TARGET} 23 | PUBLIC ${STM32CubeH7_SOURCE_DIR}/Drivers/CMSIS/Core/Include 24 | ${STM32CubeH7_SOURCE_DIR}/Drivers/CMSIS/Device/ST/STM32H7xx/Include 25 | ${STM32CubeH7_SOURCE_DIR}/Drivers/CMSIS/Include 26 | ${STM32CubeH7_SOURCE_DIR}/Drivers/STM32H7xx_HAL_Driver/Inc) 27 | 28 | # Use some other projects stm32h7xx_hal_conf.h and system_stm32h7xx.c files 29 | target_include_directories( 30 | ${TARGET} 31 | PUBLIC 32 | ${STM32CubeH7_SOURCE_DIR}/Projects/NUCLEO-H743ZI/Applications/FreeRTOS/FreeRTOS_MPU/Inc 33 | ) 34 | target_sources( 35 | ${TARGET} 36 | PRIVATE 37 | ${STM32CubeH7_SOURCE_DIR}/Projects/NUCLEO-H743ZI/Applications/FreeRTOS/FreeRTOS_MPU/Src/system_stm32h7xx.c 38 | ) 39 | 40 | target_common_warnings(${TARGET} PRIVATE) 41 | target_common_errors(${TARGET} PRIVATE) 42 | 43 | target_link_libraries(${TARGET} PRIVATE DCC::DCC) 44 | 45 | target_link_libraries( 46 | ${TARGET} PRIVATE --specs=nano.specs -Wl,--gc-sections,-Map=${TARGET}.map 47 | -T${CMAKE_CURRENT_SOURCE_DIR}/STM32H743ZITX_FLASH.ld) 48 | 49 | # Create .hex files post build 50 | add_custom_command( 51 | TARGET ${TARGET} 52 | POST_BUILD 53 | COMMAND ${CMAKE_OBJCOPY} -O ihex ${TARGET} ${TARGET}.hex 54 | COMMAND ${CMAKE_OBJDUMP} --source --all-headers --demangle --line-numbers 55 | --wide ${TARGET} > ${TARGET}.lst 56 | COMMAND ${CMAKE_SIZE} --format=berkeley ${TARGET}) 57 | endfunction() 58 | 59 | add_stm32_target(DCCStm32Decoder) 60 | add_stm32_target(DCCStm32CommandStation) 61 | -------------------------------------------------------------------------------- /tests/rx/advanced_operations.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | // Speed, direction and functions F7-F0 4 | TEST_F(RxTest, speed_direction_and_functions_f7_f0) { 5 | EXPECT_CALL(_mock, direction(_addrs.primary.value, dcc::Forward)); 6 | EXPECT_CALL(_mock, 7 | speed(_addrs.primary.value, dcc::scale_speed<126>(10 - 1))); 8 | EXPECT_CALL(_mock, function(_addrs.primary.value, 0xFFu, 0b0000'1010u)); 9 | ReceiveAndExecute( 10 | make_advanced_operations_speed_direction_and_functions_packet( 11 | _addrs.primary, dcc::Forward << 7u | 10u, 0b0000'1010u)); 12 | } 13 | 14 | // Speed, direction and functions F23-F0 15 | TEST_F(RxTest, speed_direction_and_functions_f23_f0) { 16 | EXPECT_CALL(_mock, direction(_addrs.primary.value, dcc::Forward)); 17 | EXPECT_CALL(_mock, 18 | speed(_addrs.primary.value, dcc::scale_speed<126>(10 - 1))); 19 | EXPECT_CALL(_mock, function(_addrs.primary.value, 0xFFu << 0u, 1u << 0u)); 20 | EXPECT_CALL(_mock, function(_addrs.primary.value, 0xFFu << 8u, 2u << 8u)); 21 | EXPECT_CALL(_mock, function(_addrs.primary.value, 0xFFu << 16u, 3u << 16u)); 22 | ReceiveAndExecute( 23 | make_advanced_operations_speed_direction_and_functions_packet( 24 | _addrs.primary, dcc::Forward << 7u | 10u, 1u, 2u, 3u)); 25 | } 26 | 27 | // 126 speed steps command forward 28 | TEST_F(RxTest, _126_speed_steps_fwd) { 29 | EXPECT_CALL(_mock, direction(_addrs.primary.value, dcc::Forward)); 30 | EXPECT_CALL(_mock, 31 | speed(_addrs.primary.value, dcc::scale_speed<126>(10 - 1))); 32 | ReceiveAndExecute(make_advanced_operations_speed_packet( 33 | _addrs.primary, dcc::Forward << 7u | 10u)); 34 | } 35 | 36 | // 126 speed steps command forward 37 | TEST_F(RxTest, _126_speed_steps_fwd_wrong_packet_length) { 38 | EXPECT_CALL(_mock, direction(_addrs.primary.value, dcc::Forward)).Times(0); 39 | EXPECT_CALL(_mock, speed(_addrs.primary.value, dcc::scale_speed<126>(10 - 1))) 40 | .Times(0); 41 | ReceiveAndExecute( 42 | TinkerWithPacketLength(make_advanced_operations_speed_packet( 43 | _addrs.primary, dcc::Forward << 7u | 10u))); 44 | } 45 | 46 | // 126 speed steps command backward 47 | TEST_F(RxTest, _126_speed_steps_bwd) { 48 | EXPECT_CALL(_mock, direction(_addrs.primary.value, dcc::Backward)); 49 | EXPECT_CALL(_mock, speed(_addrs.primary.value, _)); 50 | ReceiveAndExecute(make_advanced_operations_speed_packet( 51 | _addrs.primary, dcc::Backward << 7u | 10u)); 52 | } 53 | -------------------------------------------------------------------------------- /include/dcc/tx/timings.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Transmit timings 6 | /// 7 | /// \file dcc/tx/timings.hpp 8 | /// \author Vincent Hamp 9 | /// \date 31/01/2023 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include "../packet.hpp" 20 | #include "config.hpp" 21 | 22 | namespace dcc::tx { 23 | 24 | using Timings = 25 | ztl::inplace_vector; 30 | 31 | /// Convert bytes into timings 32 | /// 33 | /// \param bytes Bytes 34 | /// \param cfg Configuration 35 | /// \return Timings 36 | constexpr Timings bytes2timings(std::span bytes, 37 | Config cfg = {}) { 38 | Timings timings{}; 39 | auto first{begin(timings)}; 40 | 41 | // Preamble 42 | auto const preamble_count{cfg.num_preamble * 2uz}; 43 | first = 44 | std::ranges::fill_n(first, 45 | static_cast(preamble_count), 46 | cfg.bit1_duration); 47 | 48 | // Data 49 | for (auto const byte : bytes) { 50 | // Startbit 51 | first = std::ranges::fill_n(first, 2uz, cfg.bit0_duration); 52 | for (auto i{CHAR_BIT}; i-- > 0;) { 53 | auto const bit{byte & (1u << i) ? cfg.bit1_duration : cfg.bit0_duration}; 54 | first = std::ranges::fill_n(first, 2uz, bit); 55 | } 56 | } 57 | 58 | // Endbit 59 | first = std::ranges::fill_n(first, 2uz, cfg.bit1_duration); 60 | 61 | // Size 62 | timings.resize(static_cast( 63 | std::ranges::distance(cbegin(timings), first))); 64 | 65 | return timings; 66 | } 67 | 68 | /// Convert packet into timings 69 | /// 70 | /// \param packet Packet 71 | /// \param cfg Configuration 72 | /// \return Timings 73 | constexpr Timings packet2timings(Packet const& packet, Config cfg = {}) { 74 | return bytes2timings({cbegin(packet), size(packet)}, cfg); 75 | } 76 | 77 | } // namespace dcc::tx 78 | -------------------------------------------------------------------------------- /tests/rx/consist.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | TEST_F(RxTest, consist_shall_not_act_on_cv_manipulation) { 4 | _cvs[19uz - 1uz] = static_cast(_addrs.consist); 5 | SetUp(); 6 | 7 | // Don't write any CV which might trigger config (e.g. 1, 28, ...)! 8 | auto cv_addr{RandomInterval(30u, smath::pow(2u, 10u) - 1u)}; 9 | auto byte{RandomInterval(0u, 255u)}; 10 | 11 | EXPECT_CALL(_mock, 12 | writeCv(Matcher(cv_addr), 13 | Matcher(byte), 14 | Matcher>(_))) 15 | .Times(0); 16 | ReceiveAndExecuteTwice( 17 | dcc::make_cv_access_long_write_packet(_addrs.consist, cv_addr, byte)); 18 | } 19 | 20 | TEST_F(RxTest, consist_direction) { 21 | _cvs[19uz - 1uz] = static_cast(_addrs.consist); 22 | _cvs[29uz - 1uz] = static_cast(_cvs[29uz - 1uz]); 23 | SetUp(); 24 | 25 | EXPECT_CALL(_mock, direction(_addrs.consist.value, dcc::Forward)); 26 | EXPECT_CALL(_mock, speed(_addrs.consist.value, _)); 27 | ReceiveAndExecute(make_advanced_operations_speed_packet( 28 | _addrs.consist, dcc::Forward << 7u | 10u)); 29 | } 30 | 31 | TEST_F(RxTest, consist_direction_reversed) { 32 | _cvs[19uz - 1uz] = static_cast(1u << 7u | _addrs.consist); 33 | _cvs[29uz - 1uz] = static_cast(_cvs[29uz - 1uz]); 34 | SetUp(); 35 | 36 | EXPECT_CALL(_mock, direction(_addrs.consist.value, dcc::Backward)); 37 | EXPECT_CALL(_mock, speed(_addrs.consist.value, _)); 38 | ReceiveAndExecute(make_advanced_operations_speed_packet( 39 | _addrs.consist, dcc::Forward << 7u | 10u)); 40 | } 41 | 42 | TEST_F(RxTest, consist_only_primary_direction_reversed) { 43 | _cvs[19uz - 1uz] = static_cast(_addrs.consist); 44 | _cvs[29uz - 1uz] = static_cast(_cvs[29uz - 1uz] | 0b1u); 45 | SetUp(); 46 | 47 | EXPECT_CALL(_mock, direction(_addrs.consist.value, dcc::Backward)); 48 | EXPECT_CALL(_mock, speed(_addrs.consist.value, _)); 49 | ReceiveAndExecute(make_advanced_operations_speed_packet( 50 | _addrs.consist, dcc::Forward << 7u | 10u)); 51 | } 52 | 53 | TEST_F(RxTest, consist_and_primary_direction_reversed) { 54 | _cvs[19uz - 1uz] = static_cast(1u << 7u | _addrs.consist); 55 | _cvs[29uz - 1uz] = static_cast(_cvs[29uz - 1uz] | 0b1u); 56 | SetUp(); 57 | 58 | EXPECT_CALL(_mock, direction(_addrs.consist.value, dcc::Forward)); 59 | EXPECT_CALL(_mock, speed(_addrs.consist.value, _)); 60 | ReceiveAndExecute(make_advanced_operations_speed_packet( 61 | _addrs.consist, dcc::Forward << 7u | 10u)); 62 | } 63 | -------------------------------------------------------------------------------- /include/rmt_dcc_encoder.h: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// RMT DCC encoder 6 | /// 7 | /// \file rmt_dcc_encoder.h 8 | /// \author Vincent Hamp 9 | /// \date 08/01/2023 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | #ifdef __cplusplus 16 | extern "C" { 17 | #endif 18 | 19 | /// DCC encoder configuration 20 | typedef struct { 21 | /// Number of preamble bits [17, 30] 22 | uint8_t num_preamble; 23 | 24 | /// Optional duration of BiDi cutout bit 0 || [57, 61] 25 | uint8_t bidibit_duration; 26 | 27 | /// Duration of 1 bit [56, 60] 28 | uint8_t bit1_duration; 29 | 30 | /// Duration of 0 bit [97, 114] 31 | uint8_t bit0_duration; 32 | 33 | /// Duration of end bit [0, 60] 34 | /// 35 | /// This is mostly to work around the limitations of 36 | /// https://github.com/espressif/esp-idf/issues/13003 37 | uint8_t endbit_duration; 38 | 39 | struct { 40 | /// Level of the first bit 41 | /// 42 | /// The name is identical to that of Espressif's RMT driver 43 | /// rmt_symbol_word_t structure. 44 | /// 45 | /// ```c 46 | /// typedef union { 47 | /// struct { 48 | /// uint16_t duration0 : 15; /*!< Duration of level0 */ 49 | /// uint16_t level0 : 1; /*!< Level of the first part */ 50 | /// uint16_t duration1 : 15; /*!< Duration of level1 */ 51 | /// uint16_t level1 : 1; /*!< Level of the second part */ 52 | /// }; 53 | /// uint32_t val; /*!< Equivalent unsigned value for the RMT symbol */ 54 | /// } rmt_symbol_word_t; 55 | /// ``` 56 | bool level0 : 1; 57 | 58 | /// ZIMO 0 59 | bool zimo0 : 1; 60 | } flags; 61 | } dcc_encoder_config_t; 62 | 63 | /// Create RMT DCC encoder which encodes DCC byte stream into RMT symbols 64 | /// 65 | /// \param config DCC encoder configuration 66 | /// \param ret_encoder Returned encoder handle 67 | /// \retval ESP_OK Create RMT DCC encoder successfully 68 | /// \retval ESP_ERR_INVALID_ARG Create RMT DCC encoder failed because of 69 | /// invalid argument 70 | /// \retval ESP_ERR_NO_MEM Create RMT DCC encoder failed because out of 71 | /// memory 72 | /// \retval ESP_FAIL Create RMT DCC encoder failed because of other 73 | /// error 74 | esp_err_t rmt_new_dcc_encoder(dcc_encoder_config_t const* config, 75 | rmt_encoder_handle_t* ret_encoder); 76 | 77 | #ifdef __cplusplus 78 | } 79 | #endif -------------------------------------------------------------------------------- /examples/repl/src/decoder.cpp: -------------------------------------------------------------------------------- 1 | #include "decoder.hpp" 2 | #include 3 | 4 | // Make the prompt look nice again after output 5 | #define PROMPTENDL \ 6 | "\ndcc> "; \ 7 | std::cout << std::flush 8 | 9 | Decoder::Decoder() { 10 | _cvs[29uz - 1uz] = 0b10u; // Decoder configuration 11 | _cvs[1uz - 1uz] = 3u; // Primary address 12 | _cvs[8uz - 1uz] = DCC_MANUFACTURER_ID; // Manufacturer ID 13 | } 14 | 15 | void Decoder::direction(uint16_t addr, bool dir) { 16 | cli::Cli::cout() << "Address " << addr << ": set direction " 17 | << (dir ? "forward" : "backward") << PROMPTENDL; 18 | } 19 | 20 | void Decoder::speed(uint16_t addr, int32_t speed) { 21 | cli::Cli::cout() << "Address " << addr << ": set speed " << speed 22 | << PROMPTENDL; 23 | } 24 | 25 | void Decoder::function(uint16_t addr, uint32_t mask, uint32_t state) { 26 | auto const f_high{std::bit_width(mask) - 1}; 27 | auto const f_low{std::countr_zero(mask)}; 28 | cli::Cli::cout() << "Address " << addr << ": f" << f_high << "-" 29 | << "f" << f_low << " = " 30 | << "0b"; 31 | for (auto i{f_high}; i >= f_low; --i) 32 | cli::Cli::cout() << static_cast(state & (1u << i)); 33 | cli::Cli::cout() << PROMPTENDL; 34 | } 35 | 36 | void Decoder::serviceModeHook(bool) {} 37 | 38 | void Decoder::serviceAck() {} 39 | 40 | void Decoder::transmitBiDi(std::span) {} 41 | 42 | uint8_t Decoder::readCv(uint32_t cv_addr, [[maybe_unused]] uint8_t byte) { 43 | auto const red_byte{ 44 | static_cast(cv_addr < size(_cvs) ? _cvs[cv_addr] : 0u)}; 45 | cli::Cli::cout() << "Read CV byte " << cv_addr 46 | << "==" << static_cast(red_byte) << PROMPTENDL; 47 | return red_byte; 48 | } 49 | 50 | uint8_t Decoder::writeCv(uint32_t cv_addr, uint8_t byte) { 51 | cli::Cli::cout() << "Write CV byte " << cv_addr << "=" 52 | << static_cast(byte) << PROMPTENDL; 53 | return _cvs[cv_addr] = byte; 54 | } 55 | 56 | bool Decoder::readCv(uint32_t cv_addr, 57 | [[maybe_unused]] bool bit, 58 | uint32_t pos) { 59 | auto const red_bit{static_cast(_cvs[cv_addr] & (1u << pos))}; 60 | cli::Cli::cout() << "Read CV bit " << cv_addr << ":" << pos << "==" << red_bit 61 | << PROMPTENDL; 62 | return red_bit; 63 | } 64 | 65 | bool Decoder::writeCv(uint32_t cv_addr, bool bit, uint32_t pos) { 66 | _cvs[cv_addr] = 67 | static_cast((_cvs[cv_addr] & ~(1u << pos)) | (bit << pos)); 68 | auto const red_bit{static_cast(_cvs[cv_addr] & (1u << pos))}; 69 | cli::Cli::cout() << "Write CV bit " << cv_addr << ":" << pos << "=" << red_bit 70 | << PROMPTENDL; 71 | return red_bit; 72 | } 73 | -------------------------------------------------------------------------------- /tests/rx/app_dyn.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | using namespace dcc::bidi; 4 | 5 | TEST_F(RxTest, app_dyn) { 6 | // Send whatever packet to get last received address to match primary 7 | Receive(make_function_group_f4_f0_packet(_addrs.primary, 10u)); 8 | 9 | EXPECT_LT(DCC_RX_BIDI_DEQUE_SIZE, 42uz); 10 | 11 | // Add more datagrams than would fit the queue 12 | for (auto i{0uz}; i < 42u; ++i) 13 | _mock.datagram(DirectionStatusByte{static_cast(i)}); 14 | 15 | auto i{0uz}; 16 | 17 | { 18 | // First datagram after deque release is always QoS 19 | auto first{encode_datagram(make_datagram(7u, 0u << 6u | 7u))}; 20 | auto second{encode_datagram(make_datagram(7u, i++ << 6u | 27u))}; 21 | std::vector datagram; 22 | std::ranges::copy(first, std::back_inserter(datagram)); 23 | std::ranges::copy(second, std::back_inserter(datagram)); 24 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(datagram))).Times(1); 25 | _mock.biDiChannel2(); 26 | } 27 | 28 | { 29 | auto third{encode_datagram(make_datagram(7u, i++ << 6u | 27u))}; 30 | auto fourth{encode_datagram(make_datagram(7u, i++ << 6u | 27u))}; 31 | std::vector datagram; 32 | std::ranges::copy(third, std::back_inserter(datagram)); 33 | std::ranges::copy(fourth, std::back_inserter(datagram)); 34 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(datagram))).Times(1); 35 | _mock.biDiChannel2(); 36 | } 37 | 38 | { 39 | auto fifth{encode_datagram(make_datagram(7u, i++ << 6u | 27u))}; 40 | auto sixt{encode_datagram(make_datagram(7u, i++ << 6u | 27u))}; 41 | std::vector datagram; 42 | std::ranges::copy(fifth, std::back_inserter(datagram)); 43 | std::ranges::copy(sixt, std::back_inserter(datagram)); 44 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(datagram))).Times(1); 45 | _mock.biDiChannel2(); 46 | } 47 | 48 | { 49 | // Last datagram which fit into deque 50 | auto seventh{ 51 | encode_datagram(make_datagram(7u, i++ << 6u | 27u))}; 52 | EXPECT_EQ(DCC_RX_BIDI_DEQUE_SIZE, 7uz); 53 | std::vector datagram; 54 | std::ranges::copy(seventh, std::back_inserter(datagram)); 55 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(datagram))).Times(1); 56 | _mock.biDiChannel2(); 57 | } 58 | 59 | { 60 | // Deque is empty again at this point 61 | EXPECT_CALL(_mock, transmitBiDi(_)).Times(0); 62 | _mock.biDiChannel2(); 63 | } 64 | } 65 | 66 | TEST_F(RxTest, dont_transmit_following_foreign_address) { 67 | // Send whatever packet to foreign address 68 | Receive(dcc::make_function_group_f4_f0_packet(1817u, 10u)); 69 | 70 | // Add more datagrams than would fit the queue 71 | for (auto i{0uz}; i < 42u; ++i) 72 | _mock.datagram(DirectionStatusByte{static_cast(i)}); 73 | 74 | EXPECT_CALL(_mock, transmitBiDi(_)).Times(0); 75 | _mock.biDiChannel2(); 76 | } 77 | -------------------------------------------------------------------------------- /include/dcc/instruction.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Instruction 6 | /// 7 | /// \file dcc/instruction.hpp 8 | /// \author Vincent Hamp 9 | /// \date 04/01/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include "packet.hpp" 16 | 17 | namespace dcc { 18 | 19 | enum struct Instruction : uint8_t { 20 | UnknownService, ///< Instruction is unknown or service 21 | DecoderControl, ///< Instruction is decoder control 22 | ConsistControl, ///< Instruction is consist control 23 | AdvancedOperations, ///< Instruction is advanced operations 24 | SpeedDirection, ///< Instruction is speed and direction 25 | FunctionGroup, ///< Instruction is function group 26 | FeatureExpansion, ///< Instruction is feature expansion 27 | CvLong, ///< Instruction is long type CV access 28 | CvShort, ///< Instruction is short type CV access 29 | Logon ///< Instruction is logon 30 | }; 31 | 32 | /// Decode instruction 33 | /// 34 | /// \tparam InputIt std::input_iterator 35 | /// \param first Beginning of the range to decode from 36 | /// \return Instruction 37 | template 38 | constexpr Instruction decode_instruction(InputIt first) { 39 | switch (*first & 0xF0u) { 40 | case 0b0000'0000u: return Instruction::DecoderControl; 41 | case 0b0001'0000u: return Instruction::ConsistControl; 42 | case 0b0010'0000u: [[fallthrough]]; 43 | case 0b0011'0000u: return Instruction::AdvancedOperations; 44 | case 0b0100'0000u: [[fallthrough]]; 45 | case 0b0101'0000u: [[fallthrough]]; 46 | case 0b0110'0000u: [[fallthrough]]; 47 | case 0b0111'0000u: return Instruction::SpeedDirection; 48 | case 0b1000'0000u: [[fallthrough]]; 49 | case 0b1001'0000u: [[fallthrough]]; 50 | case 0b1010'0000u: [[fallthrough]]; 51 | case 0b1011'0000u: return Instruction::FunctionGroup; 52 | case 0b1100'0000u: [[fallthrough]]; 53 | case 0b1101'0000u: return Instruction::FeatureExpansion; 54 | case 0b1110'0000u: return Instruction::CvLong; 55 | case 0b1111'0000u: return Instruction::CvShort; 56 | default: break; 57 | } 58 | if (*first == 0b1111'1110u) return Instruction::Logon; 59 | return Instruction::UnknownService; 60 | } 61 | 62 | /// Decode instruction 63 | /// 64 | /// \param bytes Raw bytes 65 | /// \return Instruction 66 | constexpr Instruction decode_instruction(std::span bytes) { 67 | return decode_instruction(cbegin(bytes)); 68 | } 69 | 70 | /// Decode instruction 71 | /// 72 | /// \param packet Packet 73 | /// \return Instruction 74 | constexpr Instruction decode_instruction(Packet const& packet) { 75 | auto const offset{packet[0uz] >= 128u && packet[0uz] <= 252u}; 76 | return decode_instruction(cbegin(packet) + 1 + offset); 77 | } 78 | 79 | } // namespace dcc 80 | -------------------------------------------------------------------------------- /include/dcc/crc8.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// CRC8 6 | /// 7 | /// \file dcc/crc8.hpp 8 | /// \author Vincent Hamp 9 | /// \date 04/01/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include "packet.hpp" 17 | 18 | namespace dcc { 19 | 20 | namespace detail { 21 | 22 | inline constexpr std::array crc8_lut{ 23 | 0x00u, 0x5Eu, 0xBCu, 0xE2u, 0x61u, 0x3Fu, 0xDDu, 0x83u, 0xC2u, 0x9Cu, 0x7Eu, 24 | 0x20u, 0xA3u, 0xFDu, 0x1Fu, 0x41u, 0x9Du, 0xC3u, 0x21u, 0x7Fu, 0xFCu, 0xA2u, 25 | 0x40u, 0x1Eu, 0x5Fu, 0x01u, 0xE3u, 0xBDu, 0x3Eu, 0x60u, 0x82u, 0xDCu, 0x23u, 26 | 0x7Du, 0x9Fu, 0xC1u, 0x42u, 0x1Cu, 0xFEu, 0xA0u, 0xE1u, 0xBFu, 0x5Du, 0x03u, 27 | 0x80u, 0xDEu, 0x3Cu, 0x62u, 0xBEu, 0xE0u, 0x02u, 0x5Cu, 0xDFu, 0x81u, 0x63u, 28 | 0x3Du, 0x7Cu, 0x22u, 0xC0u, 0x9Eu, 0x1Du, 0x43u, 0xA1u, 0xFFu, 0x46u, 0x18u, 29 | 0xFAu, 0xA4u, 0x27u, 0x79u, 0x9Bu, 0xC5u, 0x84u, 0xDAu, 0x38u, 0x66u, 0xE5u, 30 | 0xBBu, 0x59u, 0x07u, 0xDBu, 0x85u, 0x67u, 0x39u, 0xBAu, 0xE4u, 0x06u, 0x58u, 31 | 0x19u, 0x47u, 0xA5u, 0xFBu, 0x78u, 0x26u, 0xC4u, 0x9Au, 0x65u, 0x3Bu, 0xD9u, 32 | 0x87u, 0x04u, 0x5Au, 0xB8u, 0xE6u, 0xA7u, 0xF9u, 0x1Bu, 0x45u, 0xC6u, 0x98u, 33 | 0x7Au, 0x24u, 0xF8u, 0xA6u, 0x44u, 0x1Au, 0x99u, 0xC7u, 0x25u, 0x7Bu, 0x3Au, 34 | 0x64u, 0x86u, 0xD8u, 0x5Bu, 0x05u, 0xE7u, 0xB9u, 0x8Cu, 0xD2u, 0x30u, 0x6Eu, 35 | 0xEDu, 0xB3u, 0x51u, 0x0Fu, 0x4Eu, 0x10u, 0xF2u, 0xACu, 0x2Fu, 0x71u, 0x93u, 36 | 0xCDu, 0x11u, 0x4Fu, 0xADu, 0xF3u, 0x70u, 0x2Eu, 0xCCu, 0x92u, 0xD3u, 0x8Du, 37 | 0x6Fu, 0x31u, 0xB2u, 0xECu, 0x0Eu, 0x50u, 0xAFu, 0xF1u, 0x13u, 0x4Du, 0xCEu, 38 | 0x90u, 0x72u, 0x2Cu, 0x6Du, 0x33u, 0xD1u, 0x8Fu, 0x0Cu, 0x52u, 0xB0u, 0xEEu, 39 | 0x32u, 0x6Cu, 0x8Eu, 0xD0u, 0x53u, 0x0Du, 0xEFu, 0xB1u, 0xF0u, 0xAEu, 0x4Cu, 40 | 0x12u, 0x91u, 0xCFu, 0x2Du, 0x73u, 0xCAu, 0x94u, 0x76u, 0x28u, 0xABu, 0xF5u, 41 | 0x17u, 0x49u, 0x08u, 0x56u, 0xB4u, 0xEAu, 0x69u, 0x37u, 0xD5u, 0x8Bu, 0x57u, 42 | 0x09u, 0xEBu, 0xB5u, 0x36u, 0x68u, 0x8Au, 0xD4u, 0x95u, 0xCBu, 0x29u, 0x77u, 43 | 0xF4u, 0xAAu, 0x48u, 0x16u, 0xE9u, 0xB7u, 0x55u, 0x0Bu, 0x88u, 0xD6u, 0x34u, 44 | 0x6Au, 0x2Bu, 0x75u, 0x97u, 0xC9u, 0x4Au, 0x14u, 0xF6u, 0xA8u, 0x74u, 0x2Au, 45 | 0xC8u, 0x96u, 0x15u, 0x4Bu, 0xA9u, 0xF7u, 0xB6u, 0xE8u, 0x0Au, 0x54u, 0xD7u, 46 | 0x89u, 0x6Bu, 0x35u}; 47 | 48 | } // namespace detail 49 | 50 | /// CRC8 (Dallas/Maxim) 51 | /// 52 | /// The polynomial representations is 0x31. 53 | /// 54 | /// \param byte Next byte for CRC calculation 55 | /// \return CRC8 56 | constexpr uint8_t crc8(uint8_t byte) { return detail::crc8_lut[byte]; } 57 | 58 | /// CRC8 (Dallas/Maxim) 59 | /// 60 | /// The polynomial representations is 0x31. 61 | /// 62 | /// \param bytes Bytes to calculate CRC8 for 63 | /// \return CRC8 64 | constexpr uint8_t crc8(std::span bytes) { 65 | return std::accumulate( 66 | cbegin(bytes), 67 | cend(bytes), 68 | static_cast(0u), 69 | [](uint8_t a, uint8_t b) { return crc8(static_cast(a ^ b)); }); 70 | } 71 | 72 | /// CRC8 (Dallas/Maxim) 73 | /// 74 | /// The polynomial representations is 0x31. 75 | /// 76 | /// \param packet Packet 77 | /// \return CRC8 78 | constexpr uint8_t crc8(Packet const& packet) { 79 | // Packet checksum is not part of CRC 80 | return crc8({cbegin(packet), size(packet) - 1uz}); 81 | } 82 | 83 | } // namespace dcc 84 | -------------------------------------------------------------------------------- /tests/rx/app_tos.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "rx_test.hpp" 3 | 4 | using namespace dcc::bidi; 5 | using namespace std::chrono_literals; 6 | 7 | TEST_F(RxTest, app_tos_basic_address) { 8 | // Does not require CV28:1 9 | _cvs[28uz - 1uz] = static_cast(_cvs[28uz - 1uz] & 0b1111'11101u); 10 | SetUp(); 11 | 12 | // Make sure to get past backoff (see RCN-218) 13 | for (auto i{0.0}; i < 30.0 / 10E-3; ++i) 14 | LeaveCutout()->Execute()->Receive( 15 | dcc::make_binary_state_short_packet(0u, 2u)); 16 | 17 | // Make datagram 18 | auto adr_high{encode_datagram(make_datagram(1u, 0u))}; 19 | auto adr_low{encode_datagram( 20 | make_datagram(2u, static_cast(_addrs.primary)))}; 21 | auto time{encode_datagram(make_datagram(14u, 0u))}; 22 | std::array datagram{}; 23 | auto it{std::copy(cbegin(adr_high), cend(adr_high), begin(datagram))}; 24 | it = std::copy(cbegin(adr_low), cend(adr_low), it); 25 | std::copy(cbegin(time), cend(time), it); 26 | 27 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(datagram))).Times(1); 28 | _mock.biDiChannel2(); 29 | } 30 | 31 | TEST_F(RxTest, app_tos_extended_address) { 32 | _addrs.primary = {.value = 3000u, .type = dcc::Address::ExtendedLoco}; 33 | dcc::encode_address(_addrs.primary, begin(_cvs) + 17 - 1); 34 | _cvs[29uz - 1uz] = _cvs[29uz - 1uz] | ztl::mask<5u>; 35 | SetUp(); 36 | 37 | // Make sure to get past backoff (see RCN-218) 38 | for (auto i{0.0}; i < 30.0 / 10E-3; ++i) 39 | LeaveCutout()->Execute()->Receive( 40 | dcc::make_binary_state_short_packet(0u, 2u)); 41 | 42 | // Make datagram 43 | auto adr_high{encode_datagram( 44 | make_datagram(1u, 0x80u | (_addrs.primary & 0x3F00u) >> 8u))}; 45 | auto adr_low{encode_datagram( 46 | make_datagram(2u, static_cast(_addrs.primary)))}; 47 | auto time{encode_datagram(make_datagram(14u, 0u))}; 48 | std::array datagram{}; 49 | auto it{std::copy(cbegin(adr_high), cend(adr_high), begin(datagram))}; 50 | it = std::copy(cbegin(adr_low), cend(adr_low), it); 51 | std::copy(cbegin(time), cend(time), it); 52 | 53 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(datagram))).Times(1); 54 | _mock.biDiChannel2(); 55 | } 56 | 57 | TEST_F( 58 | RxTest, 59 | app_tos_time_is_from_first_packet_regardless_of_which_packet_is_answered) { 60 | SetUp(); 61 | 62 | // Make sure to get past backoff (see RCN-218) 63 | for (auto i{0.0}; i < 30.0 / 10E-3; ++i) 64 | LeaveCutout()->Execute()->Receive( 65 | dcc::make_binary_state_short_packet(0u, 2u)); 66 | 67 | // Make datagram 68 | auto adr_high{encode_datagram(make_datagram(1u, 0u))}; 69 | auto adr_low{encode_datagram( 70 | make_datagram(2u, static_cast(_addrs.primary)))}; 71 | auto time{encode_datagram(make_datagram(14u, 0u))}; 72 | std::array datagram{}; 73 | auto it{std::copy(cbegin(adr_high), cend(adr_high), begin(datagram))}; 74 | it = std::copy(cbegin(adr_low), cend(adr_low), it); 75 | std::copy(cbegin(time), cend(time), it); 76 | 77 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(datagram))).Times(1); 78 | _mock.biDiChannel2(); 79 | 80 | std::this_thread::sleep_for(1s); 81 | 82 | // Make sure to get past backoff (see RCN-218) 83 | for (auto i{0.0}; i < 30.0 / 10E-3; ++i) 84 | LeaveCutout()->Execute()->Receive( 85 | dcc::make_binary_state_short_packet(0u, 2u)); 86 | 87 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(datagram))).Times(1); 88 | _mock.biDiChannel2(); 89 | } 90 | -------------------------------------------------------------------------------- /tests/rx/app_pom.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | using namespace dcc::bidi; 4 | 5 | TEST_F(RxTest, app_pom) { 6 | auto cv_addr{RandomInterval(0u, 255u)}; 7 | auto value{RandomInterval(0u, 255u)}; 8 | 9 | EXPECT_CALL(_mock, 10 | readCv(Matcher(cv_addr), 11 | Matcher(_), 12 | Matcher>(_))) 13 | .WillOnce(InvokeArgument<2uz>(value)); 14 | 15 | auto packet{make_cv_access_long_verify_packet(_addrs.primary, cv_addr)}; 16 | Receive(packet)->LeaveCutout()->Execute()->Receive(packet); 17 | 18 | auto datagram{encode_datagram(make_datagram(0u, value))}; 19 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(datagram))).Times(1); 20 | _mock.biDiChannel2(); 21 | } 22 | 23 | TEST_F(RxTest, app_pom_disabled_with_cv28_1) { 24 | _cvs[28uz - 1uz] = static_cast(_cvs[28uz - 1uz] & 0b1111'11101u); 25 | SetUp(); 26 | 27 | auto cv_addr{RandomInterval(0u, 255u)}; 28 | auto value{RandomInterval(0u, 255u)}; 29 | 30 | EXPECT_CALL(_mock, 31 | readCv(Matcher(cv_addr), 32 | Matcher(_), 33 | Matcher>(_))) 34 | .WillOnce(InvokeArgument<2uz>(value)); 35 | 36 | auto packet{make_cv_access_long_verify_packet(_addrs.primary, cv_addr)}; 37 | Receive(packet)->LeaveCutout()->Execute()->Receive(packet); 38 | 39 | EXPECT_CALL(_mock, transmitBiDi(_)).Times(0); 40 | _mock.biDiChannel2(); 41 | } 42 | 43 | TEST_F(RxTest, app_pom_reply_on_any_packets) { 44 | auto cv_addr{RandomInterval(0u, 255u)}; 45 | auto value{RandomInterval(0u, 255u)}; 46 | 47 | EXPECT_CALL(_mock, 48 | readCv(Matcher(cv_addr), 49 | Matcher(_), 50 | Matcher>(_))) 51 | .WillOnce(InvokeArgument<2uz>(value)); 52 | 53 | auto packet{make_cv_access_long_verify_packet(_addrs.primary, cv_addr)}; 54 | Receive(packet)->LeaveCutout()->Execute(); 55 | 56 | auto other_packet_to_same_address{ 57 | make_function_group_f4_f0_packet(_addrs.primary, 0b1u)}; 58 | Receive(other_packet_to_same_address); 59 | 60 | auto datagram{encode_datagram(make_datagram(0u, value))}; 61 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(datagram))).Times(1); 62 | _mock.biDiChannel2(); 63 | } 64 | 65 | // https://github.com/ZIMO-Elektronik/DCC/issues/105 66 | TEST_F(RxTest, app_pom_clear_internal_queue_on_unknown_cv_access_packet) { 67 | auto cv_addr{RandomInterval(0u, 255u)}; 68 | auto value{RandomInterval(0u, 255u)}; 69 | 70 | EXPECT_CALL(_mock, 71 | readCv(Matcher(cv_addr), 72 | Matcher(_), 73 | Matcher>(_))) 74 | .WillOnce(InvokeArgument<2uz>(value)); 75 | 76 | auto packet{make_cv_access_long_verify_packet(_addrs.primary, cv_addr)}; 77 | Receive(packet)->LeaveCutout()->Execute(); 78 | 79 | // At this point in time there is an ID0 datagram in the internal queue. 80 | // Now send a new CV access command without invoking the callback. This 81 | // simulates a delay in a real application. 82 | EXPECT_CALL(_mock, 83 | readCv(Matcher(cv_addr + 1u), 84 | Matcher(_), 85 | Matcher>(_))); 86 | 87 | auto other_cv_packet{ 88 | make_cv_access_long_verify_packet(_addrs.primary, cv_addr + 1u)}; 89 | Receive(other_cv_packet)->LeaveCutout()->Execute()->Receive(other_cv_packet); 90 | 91 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(dcc::bidi::acks))).Times(1); 92 | _mock.biDiChannel2(); 93 | } 94 | -------------------------------------------------------------------------------- /tests/rx/rx_test.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | #include 3 | 4 | RxTest::RxTest() { 5 | _cvs[29uz - 1uz] = 0b1010u; // Decoder configuration 6 | _cvs[1uz - 1uz] = static_cast(_addrs.primary); // Primary address 7 | _cvs[19uz - 1uz] = 0u; // Consist address low byte 8 | _cvs[20uz - 1uz] = 0u; // Consist address high byte 9 | _cvs[15uz - 1uz] = 0u; // Lock 10 | _cvs[16uz - 1uz] = 0u; // Lock compare 11 | _cvs[28uz - 1uz] = 0b1000'0011u; // RailCom 12 | 13 | // Decoder ID 14 | _cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 0uz] = static_cast(_did >> 24u); 15 | _cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 1uz] = static_cast(_did >> 16u); 16 | _cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 2uz] = static_cast(_did >> 8u); 17 | _cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 3uz] = static_cast(_did >> 0u); 18 | 19 | // CID 20 | _cvs[DCC_RX_LOGON_CID_CV_ADDRESS + 0uz] = static_cast(_cid >> 8u); 21 | _cvs[DCC_RX_LOGON_CID_CV_ADDRESS + 1uz] = static_cast(_cid >> 0u); 22 | 23 | // SID 24 | _cvs[DCC_RX_LOGON_SID_CV_ADDRESS] = _sid; 25 | 26 | // Logon address 27 | _cvs[DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 0uz] = 28 | static_cast(0b1100'0000u | _addrs.logon >> 8u); 29 | _cvs[DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 1uz] = 30 | static_cast(_addrs.logon >> 0u); 31 | } 32 | 33 | RxTest::~RxTest() {} 34 | 35 | void RxTest::SetUp() { 36 | // Extended address 37 | if (_cvs[29uz - 1uz] & ztl::mask<5u>) { 38 | /// \note 39 | /// This is weird... but not having the EXPECT_CALL inside a lambda makes 40 | /// GCC 14.2.1 (and 15.2.1) hang. 41 | std::invoke([this] { 42 | EXTENDED_ADDRESS_EXPECT_CALL_READ_CV_INIT_SEQUENCE(); 43 | _mock.init(); 44 | }); 45 | } 46 | // Basic address 47 | else { 48 | std::invoke([this] { 49 | BASIC_ADDRESS_EXPECT_CALL_READ_CV_INIT_SEQUENCE(); 50 | _mock.init(); 51 | }); 52 | } 53 | } 54 | 55 | RxTest* RxTest::Receive(dcc::Packet const& packet) { 56 | auto timings{dcc::tx::packet2timings(packet)}; 57 | std::ranges::for_each_n(cbegin(timings), 58 | size(timings), 59 | [this](uint32_t time) { _mock.receive(time); }); 60 | return this; 61 | } 62 | 63 | RxTest* RxTest::BiDiChannel1() { 64 | _mock.biDiChannel1(); 65 | return this; 66 | } 67 | 68 | RxTest* RxTest::BiDiChannel2() { 69 | _mock.biDiChannel2(); 70 | return this; 71 | } 72 | 73 | RxTest* RxTest::BiDi() { return BiDiChannel1()->BiDiChannel2(); } 74 | 75 | RxTest* RxTest::LeaveCutout() { 76 | // Receive additional preamble bit before calling execute to avoid being 77 | // inside a cutout and getting execution blocked! 78 | _mock.receive(dcc::rx::Timing::Bit1); 79 | return this; 80 | } 81 | 82 | RxTest* RxTest::Execute() { 83 | _mock.execute(); 84 | return this; 85 | } 86 | 87 | void RxTest::EnterServiceMode() { 88 | EXPECT_CALL(_mock, serviceModeHook(true)); 89 | Receive(dcc::make_reset_packet())->LeaveCutout()->Execute(); 90 | } 91 | 92 | void RxTest::Logon() { 93 | EXPECT_CALL(_mock, readCv(DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 0u)) 94 | .WillRepeatedly(Return(_cvs[DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 0uz])); 95 | EXPECT_CALL(_mock, readCv(DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 1u)) 96 | .WillRepeatedly(Return(_cvs[DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 1uz])); 97 | 98 | // Enable 99 | Receive(dcc::make_logon_enable_packet(dcc::AddressGroup::Now, _cid, _sid)); 100 | } 101 | 102 | dcc::Packet RxTest::TinkerWithPacketLength(dcc::Packet packet) const { 103 | packet.back() = RandomInterval(0u, 255u); 104 | packet.push_back(dcc::exor({cbegin(packet), cend(packet)})); 105 | return packet; 106 | } 107 | -------------------------------------------------------------------------------- /tests/tx/transmit.cpp: -------------------------------------------------------------------------------- 1 | #include "tx_test.hpp" 2 | 3 | TEST_F(TxTest, initial_idle_packet) { 4 | // See Setup() 5 | } 6 | 7 | TEST_F(TxTest, consecutive_idle_packets) { 8 | auto packet{dcc::make_idle_packet()}; 9 | auto timings{dcc::tx::packet2timings(packet, _cfg)}; 10 | auto bits{(_cfg.num_preamble + // Preamble 11 | std::size(packet) * (1uz + CHAR_BIT) // Start + data 12 | + 1uz) * // End 13 | 2uz}; 14 | for (auto i{0uz}; i < bits; ++i) 15 | EXPECT_ALL_EQ(timings[static_cast(i)], 16 | _packet_mock.transmit(), 17 | _timings_mock.transmit()); 18 | } 19 | 20 | TEST_F(TxTest, consecutive_packets_without_cutout) { 21 | _cfg.flags.bidi = false; 22 | SetUp(); 23 | 24 | auto packet{dcc::make_idle_packet()}; 25 | auto timings{dcc::tx::packet2timings(packet, _cfg)}; 26 | auto bits{(_cfg.num_preamble + // Preamble 27 | std::size(packet) * (1uz + CHAR_BIT) // Start + data 28 | + 1uz) * // End 29 | 2uz}; 30 | for (auto i{0uz}; i < bits; ++i) 31 | EXPECT_ALL_EQ(timings[static_cast(i)], 32 | _packet_mock.transmit(), 33 | _timings_mock.transmit()); 34 | for (auto i{0uz}; i < bits; ++i) 35 | EXPECT_ALL_EQ(timings[static_cast(i)], 36 | _packet_mock.transmit(), 37 | _timings_mock.transmit()); 38 | } 39 | 40 | TEST_F(TxTest, consecutive_packets_with_cutout) { 41 | { 42 | auto packet{dcc::make_function_group_f12_f9_packet(3u, 0b0000'1100u)}; 43 | EXPECT_ALL_TRUE(_packet_mock.packet(packet), _timings_mock.packet(packet)); 44 | auto timings{dcc::tx::packet2timings(packet, _cfg)}; 45 | auto bits{(_cfg.num_preamble + // Preamble 46 | std::size(packet) * (1uz + CHAR_BIT) // Start + data 47 | + 1uz) * // End 48 | 2uz}; 49 | for (auto i{0uz}; i < bits; ++i) 50 | EXPECT_ALL_EQ(timings[static_cast(i)], 51 | _packet_mock.transmit(), 52 | _timings_mock.transmit()); 53 | } 54 | 55 | { 56 | EXPECT_ALL_EQ( 57 | static_cast(dcc::bidi::Timing::TCS), 58 | _packet_mock.transmit(), 59 | _timings_mock.transmit()); 60 | EXPECT_ALL_EQ(static_cast( 61 | dcc::bidi::Timing::TTS1 - dcc::bidi::Timing::TCS), 62 | _packet_mock.transmit(), 63 | _timings_mock.transmit()); 64 | EXPECT_ALL_EQ(static_cast( 65 | dcc::bidi::Timing::TTS2 - dcc::bidi::Timing::TTS1), 66 | _packet_mock.transmit(), 67 | _timings_mock.transmit()); 68 | EXPECT_ALL_EQ(static_cast( 69 | dcc::bidi::Timing::TTC2 - dcc::bidi::Timing::TTS2), 70 | _packet_mock.transmit(), 71 | _timings_mock.transmit()); 72 | EXPECT_ALL_EQ(static_cast( 73 | dcc::bidi::Timing::TCE - dcc::bidi::Timing::TTC2), 74 | _packet_mock.transmit(), 75 | _timings_mock.transmit()); 76 | } 77 | 78 | { 79 | auto packet{dcc::make_idle_packet()}; 80 | auto timings{dcc::tx::packet2timings(packet, _cfg)}; 81 | auto bits{(_cfg.num_preamble + // Preamble 82 | std::size(packet) * (1uz + CHAR_BIT) // Start + data 83 | + 1uz) * // End 84 | 2uz}; 85 | for (auto i{0uz}; i < bits; ++i) 86 | EXPECT_ALL_EQ(timings[static_cast(i)], 87 | _packet_mock.transmit(), 88 | _timings_mock.transmit()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /data/transmission.pu: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | mode compact 4 | scale 300 as 50 pixels 5 | hide time-axis 6 | 7 | binary "P" as P 8 | P is 50 pixels height 9 | 10 | binary "N" as N 11 | N is 50 pixels height 12 | 13 | concise " " as RX 14 | 15 | @P 16 | 0 is high 17 | +58 is low 18 | +58 is high 19 | +58 is low 20 | +58 is high 21 | +58 is low 22 | +58 is high 23 | +58 is low 24 | +58 is high 25 | +58 is low 26 | +58 is high 27 | +58 is low 28 | +58 is high 29 | +58 is low 30 | +58 is high 31 | +58 is low 32 | +58 is high 33 | +58 is low 34 | +58 is high 35 | +58 is low 36 | +58 is high 37 | +58 is low 38 | +58 is high 39 | +58 is low 40 | +58 is high 41 | +58 is low 42 | +58 is high 43 | +58 is low 44 | +58 is high 45 | +58 is low 46 | +58 is high 47 | +58 is low 48 | +58 is high 49 | +58 is low 50 | +58 is high 51 | 52 | +100 is low 53 | +100 is high 54 | 55 | +100 is low 56 | +100 is high 57 | +100 is low 58 | +100 is high 59 | +100 is low 60 | +100 is high 61 | +100 is low 62 | +100 is high 63 | +100 is low 64 | +100 is high 65 | +100 is low 66 | +100 is high 67 | +58 is low 68 | +58 is high 69 | +58 is low 70 | +58 is high 71 | 72 | +100 is low 73 | +100 is high 74 | 75 | +58 is low 76 | +58 is high 77 | +100 is low 78 | +100 is high 79 | +58 is low 80 | +58 is high 81 | +58 is low 82 | +58 is high 83 | +58 is low 84 | +58 is high 85 | +58 is low 86 | +58 is high 87 | +100 is low 88 | +100 is high 89 | +100 is low 90 | +100 is high 91 | 92 | +100 is low 93 | +100 is high 94 | 95 | +58 is low 96 | +58 is high 97 | +100 is low 98 | +100 is high 99 | +58 is low 100 | +58 is high 101 | +58 is low 102 | +58 is high 103 | +58 is low 104 | +58 is high 105 | +58 is low 106 | +58 is high 107 | +58 is low 108 | +58 is high 109 | +58 is low 110 | +58 is high 111 | 112 | +58 is low 113 | +58 is high 114 | 115 | +29 is low 116 | +451 is high 117 | 118 | +58 is low 119 | 6792 is high 120 | 121 | @N 122 | 0 is low 123 | +58 is high 124 | +58 is low 125 | +58 is high 126 | +58 is low 127 | +58 is high 128 | +58 is low 129 | +58 is high 130 | +58 is low 131 | +58 is high 132 | +58 is low 133 | +58 is high 134 | +58 is low 135 | +58 is high 136 | +58 is low 137 | +58 is high 138 | +58 is low 139 | +58 is high 140 | +58 is low 141 | +58 is high 142 | +58 is low 143 | +58 is high 144 | +58 is low 145 | +58 is high 146 | +58 is low 147 | +58 is high 148 | +58 is low 149 | +58 is high 150 | +58 is low 151 | +58 is high 152 | +58 is low 153 | +58 is high 154 | +58 is low 155 | +58 is high 156 | +58 is low 157 | 158 | +100 is high 159 | +100 is low 160 | 161 | +100 is high 162 | +100 is low 163 | +100 is high 164 | +100 is low 165 | +100 is high 166 | +100 is low 167 | +100 is high 168 | +100 is low 169 | +100 is high 170 | +100 is low 171 | +100 is high 172 | +100 is low 173 | +58 is high 174 | +58 is low 175 | +58 is high 176 | +58 is low 177 | 178 | +100 is high 179 | +100 is low 180 | 181 | +58 is high 182 | +58 is low 183 | +100 is high 184 | +100 is low 185 | +58 is high 186 | +58 is low 187 | +58 is high 188 | +58 is low 189 | +58 is high 190 | +58 is low 191 | +58 is high 192 | +58 is low 193 | +100 is high 194 | +100 is low 195 | +100 is high 196 | +100 is low 197 | 198 | +100 is high 199 | +100 is low 200 | 201 | +58 is high 202 | +58 is low 203 | +100 is high 204 | +100 is low 205 | +58 is high 206 | +58 is low 207 | +58 is high 208 | +58 is low 209 | +58 is high 210 | +58 is low 211 | +58 is high 212 | +58 is low 213 | +58 is high 214 | +58 is low 215 | +58 is high 216 | +58 is low 217 | 218 | +58 is high 219 | +58 is low 220 | 221 | +29 is low 222 | +451 is low 223 | 224 | +58 is high 225 | 6792 is low 226 | 227 | @RX 228 | 0 is Preamble 229 | +1972 is {hidden} 230 | +200 is "Basic Loco Address (0x03)" 231 | +1432 is {hidden} 232 | +200 is "Function Group (0xBC)" 233 | +1180 is {hidden} 234 | +200 is "Error Detection (0xBF)" 235 | +1012 is {hidden} 236 | +116 is "BiDi" 237 | +480 is {hidden} 238 | 239 | highlight 0 to 6312 #APPLICATION:execute 240 | highlight 6312 to 6792 #BUSINESS:biDiChannel1\nbiDiChannel2 241 | 242 | @enduml 243 | -------------------------------------------------------------------------------- /tests/rx/rx_test.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "rx_mock.hpp" 6 | 7 | using namespace ::testing; 8 | 9 | // Receive test fixture 10 | struct RxTest : ::testing::Test { 11 | RxTest(); 12 | virtual ~RxTest(); 13 | 14 | void SetUp() override; 15 | 16 | RxTest* Receive(dcc::Packet const& packet); 17 | RxTest* BiDiChannel1(); 18 | RxTest* BiDiChannel2(); 19 | RxTest* BiDi(); 20 | RxTest* LeaveCutout(); 21 | RxTest* Execute(); 22 | 23 | void EnterServiceMode(); 24 | 25 | void Logon(); 26 | 27 | dcc::Packet TinkerWithPacketLength(dcc::Packet packet) const; 28 | 29 | template 30 | static T RandomInterval(T min, T max) { 31 | std::mt19937 gen{std::random_device{}()}; 32 | std::uniform_int_distribution dis{min, max}; 33 | return dis(gen); 34 | } 35 | 36 | /// \todo this needs to go 37 | void ReceiveAndExecute(dcc::Packet const& packet) { 38 | Receive(packet)->LeaveCutout()->Execute(); 39 | } 40 | 41 | /// \todo this needs to go 42 | void ReceiveAndExecuteTwice(dcc::Packet const& packet) { 43 | ReceiveAndExecute(packet); 44 | ReceiveAndExecute(packet); 45 | } 46 | 47 | NiceMock _mock; 48 | dcc::Addresses _addrs{ 49 | .primary = {.value = 3u, .type = dcc::Address::BasicLoco}, 50 | .consist = {.value = 4u, .type = dcc::Address::BasicLoco}, 51 | .logon = {.value = 1000u, .type = dcc::Address::ExtendedLoco}}; 52 | std::array _cvs{}; 53 | uint32_t _did{0xAABBCCDDu}; 54 | uint16_t _cid{0xABCDu}; 55 | uint8_t _sid{0x2Au}; 56 | }; 57 | 58 | MATCHER_P(DatagramMatcher, datagram, "") { 59 | return std::equal(cbegin(datagram), cend(datagram), cbegin(arg)); 60 | } 61 | 62 | #define BASIC_ADDRESS_EXPECT_CALL_READ_CV_INIT_SEQUENCE() \ 63 | EXPECT_CALL(_mock, readCv(_)) \ 64 | .WillOnce(Return(_cvs[29uz - 1uz])) \ 65 | .WillOnce(Return(_cvs[1uz - 1uz])) \ 66 | .WillOnce(Return(_cvs[19uz - 1uz])) \ 67 | .WillOnce(Return(_cvs[20uz - 1uz])) \ 68 | .WillOnce(Return(_cvs[15uz - 1uz])) \ 69 | .WillOnce(Return(_cvs[16uz - 1uz])) \ 70 | .WillOnce(Return(_cvs[28uz - 1uz])) \ 71 | .WillOnce(Return(_cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 0uz])) \ 72 | .WillOnce(Return(_cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 1uz])) \ 73 | .WillOnce(Return(_cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 2uz])) \ 74 | .WillOnce(Return(_cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 3uz])) \ 75 | .WillOnce(Return(_cvs[DCC_RX_LOGON_CID_CV_ADDRESS + 0uz])) \ 76 | .WillOnce(Return(_cvs[DCC_RX_LOGON_CID_CV_ADDRESS + 1uz])) \ 77 | .WillOnce(Return(_cvs[DCC_RX_LOGON_SID_CV_ADDRESS])) \ 78 | .WillOnce(Return(_cvs[DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 0u])) \ 79 | .WillOnce(Return(_cvs[DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 1u])) 80 | 81 | #define EXTENDED_ADDRESS_EXPECT_CALL_READ_CV_INIT_SEQUENCE() \ 82 | EXPECT_CALL(_mock, readCv(_)) \ 83 | .WillOnce(Return(_cvs[29uz - 1uz])) \ 84 | .WillOnce(Return(_cvs[17uz - 1uz])) \ 85 | .WillOnce(Return(_cvs[18uz - 1uz])) \ 86 | .WillOnce(Return(_cvs[19uz - 1uz])) \ 87 | .WillOnce(Return(_cvs[20uz - 1uz])) \ 88 | .WillOnce(Return(_cvs[15uz - 1uz])) \ 89 | .WillOnce(Return(_cvs[16uz - 1uz])) \ 90 | .WillOnce(Return(_cvs[28uz - 1uz])) \ 91 | .WillOnce(Return(_cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 0uz])) \ 92 | .WillOnce(Return(_cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 1uz])) \ 93 | .WillOnce(Return(_cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 2uz])) \ 94 | .WillOnce(Return(_cvs[DCC_RX_LOGON_DID_CV_ADDRESS + 3uz])) \ 95 | .WillOnce(Return(_cvs[DCC_RX_LOGON_CID_CV_ADDRESS + 0uz])) \ 96 | .WillOnce(Return(_cvs[DCC_RX_LOGON_CID_CV_ADDRESS + 1uz])) \ 97 | .WillOnce(Return(_cvs[DCC_RX_LOGON_SID_CV_ADDRESS])) \ 98 | .WillOnce(Return(_cvs[DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 0u])) \ 99 | .WillOnce(Return(_cvs[DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 1u])) 100 | -------------------------------------------------------------------------------- /tests/rx/app_adr.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | using namespace dcc::bidi; 4 | 5 | TEST_F(RxTest, app_adr_alternate_primary_id1_id2) { 6 | // Make datagram 7 | auto adr_high{encode_datagram(make_datagram(1u, 0u))}; 8 | auto adr_low{encode_datagram( 9 | make_datagram(2u, static_cast(_addrs.primary)))}; 10 | 11 | // Send whatever packet to get last received address to match primary 12 | auto packet{make_function_group_f4_f0_packet(_addrs.primary, 10u)}; 13 | Receive(packet); 14 | 15 | InSequence s; 16 | for (auto i{0uz}; i < 10uz; ++i) { 17 | LeaveCutout()->Execute()->Receive(packet); 18 | 19 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(adr_high))).Times(1); 20 | _mock.biDiChannel1(); 21 | 22 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(adr_low))).Times(1); 23 | _mock.biDiChannel1(); 24 | } 25 | } 26 | 27 | TEST_F(RxTest, app_adr_alternate_logon_id1_id2) { 28 | Logon(); 29 | 30 | // Make datagram 31 | auto adr_high{encode_datagram( 32 | make_datagram(1u, 0x80u | (_addrs.logon & 0x3F00u) >> 8u))}; 33 | auto adr_low{ 34 | encode_datagram(make_datagram(2u, _addrs.logon & 0x00FFu))}; 35 | 36 | // Send whatever packet to get last received address to match primary 37 | auto packet{make_function_group_f4_f0_packet(_addrs.primary, 10u)}; 38 | Receive(packet); 39 | 40 | InSequence s; 41 | for (auto i{0uz}; i < 10uz; ++i) { 42 | LeaveCutout()->Execute()->Receive(packet); 43 | 44 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(adr_high))).Times(1); 45 | _mock.biDiChannel1(); 46 | 47 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(adr_low))).Times(1); 48 | _mock.biDiChannel1(); 49 | } 50 | } 51 | 52 | TEST_F(RxTest, app_adr_disabled_with_cv28_0) { 53 | _cvs[28uz - 1uz] = static_cast(_cvs[28uz - 1uz] & 0b1111'11110u); 54 | SetUp(); 55 | 56 | // Send whatever packet to get last received address to match primary 57 | Receive(make_function_group_f4_f0_packet(_addrs.primary, 10u)); 58 | 59 | EXPECT_CALL(_mock, transmitBiDi(_)).Times(0); 60 | Execute(); 61 | _mock.biDiChannel1(); 62 | } 63 | 64 | TEST_F(RxTest, app_adr_alternate_consist_id1_id2) { 65 | _cvs[19uz - 1uz] = static_cast(_addrs.consist); 66 | SetUp(); 67 | 68 | // Make datagram 69 | auto adr_high{encode_datagram(make_datagram(1u, 0b0110'0000u))}; 70 | auto adr_low{encode_datagram( 71 | make_datagram(2u, static_cast(_addrs.consist)))}; 72 | 73 | // Send whatever packet to get last received address to match primary 74 | auto packet{make_function_group_f4_f0_packet(_addrs.consist, 10u)}; 75 | Receive(packet); 76 | 77 | InSequence s; 78 | for (auto i{0uz}; i < 10uz; ++i) { 79 | LeaveCutout()->Execute()->Receive(packet); 80 | 81 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(adr_high))).Times(1); 82 | _mock.biDiChannel1(); 83 | 84 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(adr_low))).Times(1); 85 | _mock.biDiChannel1(); 86 | } 87 | } 88 | 89 | TEST_F(RxTest, app_adr_alternate_long_consist_id1_id2) { 90 | _cvs[19uz - 1uz] = 83u; 91 | _cvs[20uz - 1uz] = 12u; 92 | _addrs.consist = static_cast( 93 | 100u * (_cvs[20uz - 1uz] & 0b0111'1111u) + 94 | (_cvs[19uz - 1uz] & 0b0111'1111u)); 95 | SetUp(); 96 | 97 | // Make datagram 98 | auto adr_high{encode_datagram( 99 | make_datagram(1u, 0x80u | (_addrs.consist & 0x3F00u) >> 8u))}; 100 | auto adr_low{ 101 | encode_datagram(make_datagram(2u, _addrs.consist & 0x00FFu))}; 102 | 103 | // Send whatever packet to get last received address to match primary 104 | auto packet{make_function_group_f4_f0_packet(_addrs.consist, 10u)}; 105 | Receive(packet); 106 | 107 | InSequence s; 108 | for (auto i{0uz}; i < 10uz; ++i) { 109 | LeaveCutout()->Execute()->Receive(packet); 110 | 111 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(adr_high))).Times(1); 112 | _mock.biDiChannel1(); 113 | 114 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(adr_low))).Times(1); 115 | _mock.biDiChannel1(); 116 | } 117 | } 118 | 119 | TEST_F(RxTest, app_adr_broadcast) { 120 | // Make datagram 121 | auto adr_high{encode_datagram(make_datagram(1u, 0u))}; 122 | auto adr_low{encode_datagram( 123 | make_datagram(2u, static_cast(_addrs.primary)))}; 124 | 125 | // Broadcast 126 | auto packet{dcc::make_speed_and_direction_packet(0u, 0u)}; 127 | Receive(packet); 128 | 129 | InSequence s; 130 | for (auto i{0uz}; i < 10uz; ++i) { 131 | LeaveCutout()->Execute()->Receive(packet); 132 | 133 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(adr_high))).Times(0); 134 | _mock.biDiChannel1(); 135 | 136 | EXPECT_CALL(_mock, transmitBiDi(DatagramMatcher(adr_low))).Times(0); 137 | _mock.biDiChannel1(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/address.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | TEST(address, 5 | basic_and_extended_loco_address_compare_equal_based_solely_on_value) { 6 | dcc::Address short_addr{.value = 3u, .type = dcc::Address::BasicLoco}; 7 | dcc::Address long_addr{.value = 3u, .type = dcc::Address::ExtendedLoco}; 8 | EXPECT_EQ(short_addr, long_addr); 9 | EXPECT_EQ(short_addr, 3u); 10 | EXPECT_EQ(long_addr, 3u); 11 | } 12 | 13 | TEST(address, 14 | basic_loco_and_basic_accessory_address_are_not_equal_despite_same_value) { 15 | dcc::Address short_addr{.value = 3u, .type = dcc::Address::BasicLoco}; 16 | dcc::Address basic_accessory_addr{.value = 3u, 17 | .type = dcc::Address::BasicAccessory}; 18 | EXPECT_NE(short_addr, basic_accessory_addr); 19 | EXPECT_EQ(short_addr, 3u); 20 | EXPECT_EQ(basic_accessory_addr, 3u); 21 | } 22 | 23 | TEST(address, 24 | basic_and_extended_accessory_address_are_not_equal_despite_same_value) { 25 | dcc::Address basic_accessory_addr{.value = 3u, 26 | .type = dcc::Address::BasicAccessory}; 27 | dcc::Address extended_accessory_addr{.value = 3u, 28 | .type = dcc::Address::ExtendedAccessory}; 29 | EXPECT_NE(basic_accessory_addr, extended_accessory_addr); 30 | EXPECT_EQ(basic_accessory_addr, 3u); 31 | EXPECT_EQ(extended_accessory_addr, 3u); 32 | } 33 | 34 | TEST(address, decode_address) { 35 | { 36 | std::array data{}; 37 | EXPECT_EQ(dcc::decode_address(cbegin(data)), 38 | (dcc::Address{.value = 0u, .type = dcc::Address::Broadcast})); 39 | } 40 | 41 | { 42 | std::array data{0b0000'0011u}; 43 | EXPECT_EQ(dcc::decode_address(cbegin(data)), 44 | (dcc::Address{.value = 3u, .type = dcc::Address::BasicLoco})); 45 | } 46 | 47 | { 48 | std::array data{0b1011'0011u, 0b1010'0010u}; 49 | EXPECT_EQ( 50 | dcc::decode_address(cbegin(data)), 51 | (dcc::Address{.value = 1485u, .type = dcc::Address::BasicAccessory})); 52 | } 53 | 54 | { 55 | std::array data{0b1011'0011u, 0b0010'0011u}; 56 | EXPECT_EQ( 57 | dcc::decode_address(cbegin(data)), 58 | (dcc::Address{.value = 1485u, .type = dcc::Address::ExtendedAccessory})); 59 | } 60 | 61 | { 62 | std::array data{0b1101'0011u, 0b0000'1010u}; 63 | EXPECT_EQ( 64 | dcc::decode_address(cbegin(data)), 65 | (dcc::Address{.value = 4874u, .type = dcc::Address::ExtendedLoco})); 66 | } 67 | 68 | { 69 | std::array data{0b1111'1110u}; 70 | EXPECT_EQ( 71 | dcc::decode_address(cbegin(data)), 72 | (dcc::Address{.value = 254u, .type = dcc::Address::AutomaticLogon})); 73 | } 74 | 75 | { 76 | std::array data{0b1111'1111u}; 77 | EXPECT_EQ(dcc::decode_address(cbegin(data)), 78 | (dcc::Address{.value = 255u, .type = dcc::Address::Idle})); 79 | } 80 | } 81 | 82 | TEST(address, encode_address) { 83 | { 84 | std::array data{}; 85 | EXPECT_EQ(dcc::encode_address( 86 | dcc::Address{.value = 0u, .type = dcc::Address::Broadcast}, 87 | begin(data)), 88 | cbegin(data) + 1); 89 | EXPECT_EQ(data, (decltype(data){})); 90 | } 91 | 92 | { 93 | std::array data{}; 94 | EXPECT_EQ(dcc::encode_address( 95 | dcc::Address{.value = 3u, .type = dcc::Address::BasicLoco}, 96 | begin(data)), 97 | cbegin(data) + 1); 98 | EXPECT_EQ(data, (decltype(data){0b0000'0011u})); 99 | } 100 | 101 | { 102 | std::array data{}; 103 | EXPECT_EQ( 104 | dcc::encode_address( 105 | dcc::Address{.value = 1485u, .type = dcc::Address::BasicAccessory}, 106 | begin(data)), 107 | cbegin(data) + 2); 108 | EXPECT_EQ(data, (decltype(data){0b1011'0011u, 0b1010'0010u})); 109 | } 110 | 111 | { 112 | std::array data{}; 113 | EXPECT_EQ( 114 | dcc::encode_address( 115 | dcc::Address{.value = 1485u, .type = dcc::Address::ExtendedAccessory}, 116 | begin(data)), 117 | cbegin(data) + 2); 118 | EXPECT_EQ(data, (decltype(data){0b1011'0011u, 0b0010'0011u})); 119 | } 120 | 121 | { 122 | std::array data{}; 123 | EXPECT_EQ( 124 | dcc::encode_address( 125 | dcc::Address{.value = 4874u, .type = dcc::Address::ExtendedLoco}, 126 | begin(data)), 127 | cbegin(data) + 2); 128 | EXPECT_EQ(data, (decltype(data){0b1101'0011u, 0b0000'1010u})); 129 | } 130 | 131 | { 132 | std::array data{}; 133 | EXPECT_EQ( 134 | dcc::encode_address( 135 | dcc::Address{.value = 254u, .type = dcc::Address::AutomaticLogon}, 136 | begin(data)), 137 | cbegin(data) + 1); 138 | EXPECT_EQ(data, (decltype(data){0b1111'1110u})); 139 | } 140 | 141 | { 142 | std::array data{}; 143 | EXPECT_EQ( 144 | dcc::encode_address( 145 | dcc::Address{.value = 255u, .type = dcc::Address::Idle}, begin(data)), 146 | cbegin(data) + 1); 147 | EXPECT_EQ(data, (decltype(data){0b1111'1111u})); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/rx/logon.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | // Known CID skips logon 4 | TEST_F(RxTest, skip_logon_with_known_cid) { 5 | Logon(); 6 | 7 | // Execute commands to logon address 8 | EXPECT_CALL(_mock, writeCv(_, _)).Times(7); 9 | EXPECT_CALL(_mock, direction(_addrs.primary.value, false)); 10 | EXPECT_CALL(_mock, speed(_addrs.primary.value, _)); 11 | ReceiveAndExecute( 12 | dcc::make_advanced_operations_speed_packet(_addrs.logon, 0u)); 13 | } 14 | 15 | // Unknown CID forces logon 16 | TEST_F(RxTest, logon_with_unknown_cid_basic_loco) { 17 | EXPECT_CALL(_mock, transmitBiDi(_)).Times(3 * 2); 18 | 19 | // Enable 20 | Receive(make_logon_enable_packet( 21 | dcc::AddressGroup::Now, _cid + 1u, RandomInterval(0u, 255u))); 22 | BiDi(); 23 | 24 | // Select 25 | Receive(dcc::make_logon_select_packet(DCC_MANUFACTURER_ID, _did)); 26 | BiDi(); 27 | 28 | // Assign address 42 29 | _addrs.logon = {.value = 42u, .type = dcc::Address::BasicLoco}; 30 | Receive( 31 | dcc::make_logon_assign_packet(DCC_MANUFACTURER_ID, _did, _addrs.logon)); 32 | BiDi(); 33 | 34 | // Execute commands to address 42 35 | EXPECT_CALL(_mock, writeCv(_, _)).Times(7); 36 | EXPECT_CALL(_mock, direction(_addrs.primary.value, false)); 37 | EXPECT_CALL(_mock, speed(_addrs.primary.value, _)); 38 | ReceiveAndExecute( 39 | dcc::make_advanced_operations_speed_packet(_addrs.logon, 0u)); 40 | } 41 | 42 | // Unknown CID forces logon 43 | TEST_F(RxTest, logon_with_unknown_cid_extended_loco) { 44 | EXPECT_CALL(_mock, transmitBiDi(_)).Times(3 * 2); 45 | 46 | // Enable 47 | Receive(make_logon_enable_packet( 48 | dcc::AddressGroup::Now, _cid + 1u, RandomInterval(0u, 255u))); 49 | BiDi(); 50 | 51 | // Select 52 | Receive(dcc::make_logon_select_packet(DCC_MANUFACTURER_ID, _did)); 53 | BiDi(); 54 | 55 | // Assign address 1001 56 | _addrs.logon = {.value = 1001u, .type = dcc::Address::ExtendedLoco}; 57 | Receive( 58 | dcc::make_logon_assign_packet(DCC_MANUFACTURER_ID, _did, _addrs.logon)); 59 | BiDi(); 60 | 61 | // Execute commands to address 1001 62 | EXPECT_CALL(_mock, writeCv(_, _)).Times(7); 63 | EXPECT_CALL(_mock, direction(_addrs.primary.value, false)); 64 | EXPECT_CALL(_mock, speed(_addrs.primary.value, _)); 65 | ReceiveAndExecute( 66 | dcc::make_advanced_operations_speed_packet(_addrs.logon, 0u)); 67 | } 68 | 69 | // Known CID and unknown SID doesn't skip logon 70 | TEST_F(RxTest, no_logon_with_known_cid_and_unknown_sid) { 71 | EXPECT_CALL(_mock, readCv(_)).Times(0); 72 | 73 | // Enable 74 | Receive( 75 | dcc::make_logon_enable_packet(dcc::AddressGroup::Now, _cid, _sid + 1u)); 76 | 77 | // Execute commands to logon address 78 | EXPECT_CALL(_mock, writeCv(_, _)).Times(0); 79 | EXPECT_CALL(_mock, direction(_addrs.primary.value, false)).Times(0); 80 | EXPECT_CALL(_mock, speed(_addrs.primary.value, _)).Times(0); 81 | ReceiveAndExecute(make_advanced_operations_speed_packet(_addrs.logon, 0u)); 82 | } 83 | 84 | // Known CID and SID incremented by >1 forces relogon 85 | TEST_F(RxTest, force_new_logon_with_known_cid_and_sid_plus_2) { 86 | Logon(); 87 | 88 | // Execute commands to logon address 89 | EXPECT_CALL(_mock, writeCv(_, _)).Times(7); 90 | EXPECT_CALL(_mock, direction(_addrs.primary.value, false)); 91 | EXPECT_CALL(_mock, speed(_addrs.primary.value, _)); 92 | ReceiveAndExecute( 93 | dcc::make_advanced_operations_speed_packet(_addrs.logon, 0u)); 94 | 95 | EXPECT_CALL(_mock, transmitBiDi(_)).Times(3 * 2); 96 | 97 | // Enable 98 | Receive(make_logon_enable_packet(dcc::AddressGroup::Now, _cid, _sid + 2u)); 99 | BiDi(); 100 | 101 | // Select 102 | Receive(dcc::make_logon_select_packet(DCC_MANUFACTURER_ID, _did)); 103 | BiDi(); 104 | 105 | // Assign address 1001 106 | _addrs.logon = {.value = 1001u, .type = dcc::Address::ExtendedLoco}; 107 | Receive( 108 | dcc::make_logon_assign_packet(DCC_MANUFACTURER_ID, _did, _addrs.logon)); 109 | BiDi(); 110 | 111 | // Execute commands to address 1001 112 | EXPECT_CALL(_mock, writeCv(_, _)).Times(7); 113 | EXPECT_CALL(_mock, direction(_addrs.primary.value, false)); 114 | EXPECT_CALL(_mock, speed(_addrs.primary.value, _)); 115 | ReceiveAndExecute( 116 | dcc::make_advanced_operations_speed_packet(_addrs.logon, 0u)); 117 | } 118 | 119 | // LOGON_SELECT disables LOGON_ENABLE (and ID15 datagram) 120 | TEST_F( 121 | RxTest, 122 | no_id15_datagram_after_logon_select_as_long_as_cid_and_sid_stay_the_same) { 123 | EXPECT_CALL(_mock, readCv(DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 0u)) 124 | .WillRepeatedly(Return(_cvs[DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 0uz])); 125 | EXPECT_CALL(_mock, readCv(DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 1u)) 126 | .WillRepeatedly(Return(_cvs[DCC_RX_LOGON_ADDRESS_CV_ADDRESS + 1uz])); 127 | 128 | EXPECT_CALL(_mock, transmitBiDi(_)).Times(3 * 2); 129 | 130 | // Enable, expect ID15 131 | Receive(make_logon_enable_packet(dcc::AddressGroup::Now, _cid + 1u, _sid)); 132 | BiDi(); 133 | 134 | // Select, expect ID13 135 | Receive(dcc::make_logon_select_packet(DCC_MANUFACTURER_ID, _did)); 136 | BiDi(); 137 | 138 | // Enable (again) 139 | Receive(make_logon_enable_packet(dcc::AddressGroup::Now, _cid + 1u, _sid)); 140 | BiDi(); 141 | 142 | // Enable (again with new SID), expect ID15 143 | Receive( 144 | make_logon_enable_packet(dcc::AddressGroup::Now, _cid + 1u, _sid + 1u)); 145 | BiDi(); 146 | } 147 | -------------------------------------------------------------------------------- /include/dcc/tx/timings_adapter.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Timings iterator/view which converts packet to timings 6 | /// 7 | /// \file dcc/tx/timings_adapter.hpp 8 | /// \author Vincent Hamp 9 | /// \date 28/05/2025 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include "timings.hpp" 18 | 19 | namespace dcc::tx { 20 | 21 | /// Convert Packet to Timings on-the-fly 22 | struct TimingsAdapter : std::ranges::view_interface { 23 | 24 | template 25 | struct _iterator { 26 | friend _iterator; 27 | 28 | // Types 29 | using value_type = Timings::value_type; 30 | using size_type = Timings::size_type; 31 | using difference_type = Timings::difference_type; 32 | using reference = value_type; 33 | using pointer = value_type; 34 | using iterator_category = std::input_iterator_tag; 35 | using timings_adapter_pointer = 36 | std::conditional_t; 37 | 38 | // Construct/copy/destroy 39 | constexpr _iterator() = default; 40 | constexpr _iterator(timings_adapter_pointer ptr) : _ptr{ptr} {} 41 | constexpr _iterator(_iterator const&) = default; 42 | constexpr _iterator(_iterator const& rhs) requires Const 43 | : _ptr{rhs._ptr} {} 44 | constexpr _iterator& operator=(_iterator const&) = default; 45 | constexpr _iterator& operator=(_iterator const& rhs) requires Const 46 | { 47 | _ptr = rhs._ptr; 48 | return *this; 49 | } 50 | 51 | constexpr _iterator& operator++() { 52 | ++_count; 53 | return *this; 54 | } 55 | 56 | constexpr _iterator operator++(int) { 57 | auto retval{*this}; 58 | ++(*this); 59 | return retval; 60 | } 61 | 62 | constexpr reference operator*() const { 63 | // Preamble 64 | auto const& cfg{_ptr->_cfg}; 65 | auto const preamble_count{cfg.num_preamble * 2uz}; 66 | if (_count < preamble_count) return cfg.bit1_duration; 67 | 68 | // Count without preamble 69 | auto i{_count - preamble_count}; 70 | 71 | // Index of current byte 72 | auto const& packet{_ptr->_packet}; 73 | auto const byte_index{ 74 | static_cast(i / ((1uz + CHAR_BIT) * 2uz))}; 75 | if (byte_index >= std::size(packet)) return cfg.bit1_duration; 76 | 77 | // Index of current half bit 78 | auto const hbit_index{i % ((1uz + CHAR_BIT) * 2uz)}; 79 | if (hbit_index < 2uz) return cfg.bit0_duration; 80 | 81 | // Index of current bit 82 | auto const bit_index{(hbit_index - 2uz) / 2uz}; 83 | return packet[byte_index] & 1u << (CHAR_BIT - 1uz - bit_index) 84 | ? cfg.bit1_duration 85 | : cfg.bit0_duration; 86 | } 87 | 88 | constexpr bool operator==(std::default_sentinel_t) const { 89 | return _count >= _ptr->_max_count; 90 | } 91 | 92 | private: 93 | timings_adapter_pointer _ptr{}; 94 | size_type _count{}; 95 | }; 96 | 97 | // Types 98 | using value_type = Timings::value_type; 99 | using size_type = Timings::size_type; 100 | using difference_type = Timings::difference_type; 101 | using reference = value_type; 102 | using const_reference = value_type; 103 | using pointer = value_type; 104 | using const_pointer = value_type; 105 | using iterator = _iterator; 106 | using const_iterator = _iterator; 107 | 108 | // Construct/copy/destroy 109 | constexpr TimingsAdapter() = default; 110 | constexpr TimingsAdapter(Packet const& packet, Config cfg) 111 | : _packet{packet}, _cfg{cfg}, 112 | _max_count{static_cast( 113 | (_cfg.num_preamble + std::size(_packet) * (1uz + CHAR_BIT) + 1uz) * 114 | 2uz)} {} 115 | constexpr TimingsAdapter(std::span bytes, Config cfg) 116 | : _cfg{cfg}, 117 | _max_count{static_cast( 118 | (_cfg.num_preamble + std::size(bytes) * (1uz + CHAR_BIT) + 1uz) * 119 | 2uz)} { 120 | std::ranges::copy(bytes, std::back_inserter(_packet)); 121 | } 122 | 123 | // Iterators 124 | constexpr iterator begin() { return {this}; } 125 | constexpr const_iterator begin() const { return {this}; } 126 | constexpr std::default_sentinel_t end() { return std::default_sentinel; } 127 | constexpr std::default_sentinel_t end() const { 128 | return std::default_sentinel; 129 | } 130 | constexpr const_iterator cbegin() const { return begin(); } 131 | constexpr std::default_sentinel_t cend() const { return end(); } 132 | 133 | private: 134 | Packet _packet; 135 | Config _cfg{}; 136 | size_type _max_count{}; 137 | }; 138 | 139 | constexpr auto begin(TimingsAdapter& c) -> decltype(c.begin()) { 140 | return c.begin(); 141 | } 142 | constexpr auto begin(TimingsAdapter const& c) -> decltype(c.begin()) { 143 | return c.begin(); 144 | } 145 | constexpr auto end(TimingsAdapter& c) -> decltype(c.end()) { return c.end(); } 146 | constexpr auto end(TimingsAdapter const& c) -> decltype(c.end()) { 147 | return c.end(); 148 | } 149 | constexpr auto cbegin(TimingsAdapter const& c) -> decltype(c.begin()) { 150 | return c.begin(); 151 | } 152 | constexpr auto cend(TimingsAdapter const& c) -> decltype(c.end()) { 153 | return c.end(); 154 | } 155 | 156 | } // namespace dcc::tx 157 | -------------------------------------------------------------------------------- /include/dcc/address.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Address 6 | /// 7 | /// \file dcc/address.hpp 8 | /// \author Vincent Hamp 9 | /// \date 04/01/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include "packet.hpp" 18 | 19 | namespace dcc { 20 | 21 | /// Address value and type 22 | struct Address { 23 | using value_type = uint16_t; 24 | 25 | constexpr Address& operator=(value_type const& v) { 26 | value = v; 27 | return *this; 28 | } 29 | 30 | friend constexpr bool operator==(Address const& lhs, Address const& rhs) { 31 | return (lhs.type == BasicLoco || lhs.type == ExtendedLoco) && 32 | (rhs.type == BasicLoco || rhs.type == ExtendedLoco) 33 | ? lhs.value == rhs.value 34 | : lhs.value == rhs.value && lhs.type == rhs.type; 35 | } 36 | 37 | constexpr operator value_type&() { return value; } 38 | constexpr operator value_type const&() const { return value; } 39 | 40 | value_type value{}; 41 | 42 | enum : uint8_t { 43 | UnknownService, ///< Unknown or service (=no address) 44 | Broadcast, ///< Broadcast 45 | BasicLoco, ///< Basic loco (7 bit) 46 | BasicAccessory, ///< Basic accessory (11 bit) 47 | ExtendedAccessory, ///< Extended accessory (11 bit) 48 | ExtendedLoco, ///< Extended loco (14 bit) 49 | Reserved, ///< Reserved 50 | DataTransfer, ///< Data transfer 51 | AutomaticLogon, ///< Automatic logon 52 | Idle ///< Idle 53 | } type{}; 54 | 55 | bool reversed{}; /// Direction reversed 56 | }; 57 | 58 | #pragma GCC diagnostic push 59 | #pragma GCC diagnostic ignored "-Warray-bounds" 60 | /// Decode address 61 | /// 62 | /// \tparam InputIt std::input_iterator 63 | /// \param first Beginning of the range to decode from 64 | /// \return Address 65 | template 66 | constexpr Address decode_address(InputIt first) { 67 | // 0 68 | if (*first == 0u) return {*first, Address::Broadcast}; 69 | // 1-127 70 | else if (*first <= 127u) return {*first, Address::BasicLoco}; 71 | // 128-191 72 | else if (*first <= 191u) { 73 | auto const a7_2{(*first++ & 0x3Fu) << 2u}; 74 | auto const a10_8{(~*first & 0x70u) << 4u}; 75 | auto const a1_0{(*first >> 1u) & 0x03u}; 76 | return {static_cast(a10_8 | a7_2 | a1_0), 77 | *first & 0b1000'0000u ? Address::BasicAccessory 78 | : Address::ExtendedAccessory}; 79 | } 80 | // 192-231 81 | else if (*first <= 231u) { 82 | auto const a13_8{(*first++ & 0b0011'1111u) << 8u}; 83 | auto const a7_0{*first}; 84 | return {static_cast(a13_8 | a7_0), 85 | Address::ExtendedLoco}; 86 | } 87 | // 232-252 88 | else if (*first <= 252u) 89 | return {*first, Address::Reserved}; 90 | // 253 91 | else if (*first == 253u) return {*first, Address::DataTransfer}; 92 | // 254 93 | else if (*first == 254u) return {*first, Address::AutomaticLogon}; 94 | // 255 95 | else return {*first, Address::Idle}; 96 | } 97 | #pragma GCC diagnostic pop 98 | 99 | /// Decode address 100 | /// 101 | /// \param bytes Raw bytes 102 | /// \return Address 103 | constexpr Address decode_address(std::span bytes) { 104 | return decode_address(cbegin(bytes)); 105 | } 106 | 107 | /// Decode address 108 | /// 109 | /// \param packet Packet 110 | /// \return Address 111 | constexpr Address decode_address(Packet const& packet) { 112 | return decode_address(cbegin(packet)); 113 | } 114 | 115 | /// Encode address 116 | /// 117 | /// \tparam OutputIt std::output_iterator 118 | /// \param addr Address 119 | /// \param first Beginning of the range to encode to 120 | /// \return Iterator to the element that follows the last element encoded 121 | template OutputIt> 122 | constexpr OutputIt encode_address(Address addr, OutputIt first) { 123 | switch (addr.type) { 124 | case Address::UnknownService: break; 125 | case Address::Broadcast: *first++ = 0u; break; 126 | case Address::BasicLoco: *first++ = static_cast(addr); break; 127 | case Address::BasicAccessory: 128 | *first++ = static_cast(0x80u | // 129 | ((addr >> 2u) & 0x3Fu)); // A7-2 130 | *first++ = static_cast(0x80u | // 131 | ((~addr & 0x0700u) >> 4u) | // A10-8 132 | ((addr & 0x03u) << 1u)); // A1-0 133 | break; 134 | case Address::ExtendedAccessory: 135 | *first++ = static_cast(0x80u | // 136 | ((addr >> 2u) & 0x3Fu)); // A7-2 137 | *first++ = static_cast(((~addr & 0x0700u) >> 4u) | // A10-8 138 | ((addr & 0x03u) << 1u) | // A1-0 139 | 0x01u); // 140 | break; 141 | case Address::ExtendedLoco: 142 | *first++ = static_cast(0xC0u | addr >> 8u); 143 | *first++ = static_cast(addr); 144 | break; 145 | case Address::Reserved: assert(false); break; 146 | case Address::DataTransfer: *first++ = 253u; break; 147 | case Address::AutomaticLogon: *first++ = 254u; break; 148 | case Address::Idle: *first++ = 255u; break; 149 | } 150 | return first; 151 | } 152 | 153 | } // namespace dcc 154 | -------------------------------------------------------------------------------- /tests/rx/cv_long.cpp: -------------------------------------------------------------------------------- 1 | #include "rx_test.hpp" 2 | 3 | TEST_F(RxTest, cv_long_verify_bit_service_mode) { 4 | EnterServiceMode(); 5 | 6 | // Don't write any CV which might trigger config (e.g. 1, 28, ...)! 7 | auto cv_addr{RandomInterval(30u, smath::pow(2u, 10u) - 1u)}; 8 | auto bit{RandomInterval(0u, 1u)}; 9 | auto position{RandomInterval(0u, 7u)}; 10 | auto packet{ 11 | dcc::make_cv_access_long_verify_service_packet(cv_addr, bit, position)}; 12 | 13 | // 5 or more identical packets 14 | EXPECT_CALL(_mock, readCv(cv_addr, bit, position)).WillOnce(Return(bit)); 15 | EXPECT_CALL(_mock, serviceAck()); 16 | for (auto i{0uz}; i < 5uz; ++i) ReceiveAndExecute(packet); 17 | } 18 | 19 | TEST_F(RxTest, cv_long_verify_byte_operations_mode) { 20 | auto cv_addr{RandomInterval(0u, smath::pow(2u, 10u) - 1u)}; 21 | auto packet{make_cv_access_long_verify_packet(_addrs.primary, cv_addr)}; 22 | 23 | EXPECT_CALL(_mock, 24 | readCv(Matcher(cv_addr), 25 | Matcher(_), 26 | Matcher>(_))) 27 | .WillOnce(InvokeArgument<2uz>(RandomInterval(0u, 255u))); 28 | 29 | ReceiveAndExecute(packet); 30 | } 31 | 32 | TEST_F(RxTest, cv_long_verify_byte_service_mode) { 33 | EnterServiceMode(); 34 | 35 | auto cv_addr{RandomInterval(0u, smath::pow(2u, 10u) - 1u)}; 36 | auto packet{dcc::make_cv_access_long_verify_service_packet(cv_addr, 42u)}; 37 | 38 | // 5 or more identical packets 39 | EXPECT_CALL(_mock, readCv(cv_addr, 42u)).WillOnce(Return(42u)); 40 | EXPECT_CALL(_mock, serviceAck()); 41 | for (auto i{0uz}; i < 5uz; ++i) ReceiveAndExecute(packet); 42 | } 43 | 44 | TEST_F(RxTest, cv_long_ignore_write_bit_operations_mode) { 45 | // Don't write any CV which might trigger config (e.g. 1, 28, ...)! 46 | auto cv_addr{RandomInterval(30u, smath::pow(2u, 10u) - 1u)}; 47 | auto bit{RandomInterval(0u, 1u)}; 48 | auto position{RandomInterval(0u, 7u)}; 49 | auto packet{ 50 | make_cv_access_long_write_packet(_addrs.primary, cv_addr, bit, position)}; 51 | 52 | // 2 or more identical packets 53 | EXPECT_CALL(_mock, 54 | writeCv(Matcher(cv_addr), 55 | Matcher(_), 56 | Matcher>(_))) 57 | .Times(0); 58 | for (auto i{0uz}; i < 2uz; ++i) ReceiveAndExecute(packet); 59 | } 60 | 61 | TEST_F(RxTest, cv_long_write_bit_service_mode) { 62 | EnterServiceMode(); 63 | 64 | // Don't write any CV which might trigger config (e.g. 1, 28, ...)! 65 | auto cv_addr{RandomInterval(30u, smath::pow(2u, 10u) - 1u)}; 66 | auto bit{RandomInterval(0u, 1u)}; 67 | auto position{RandomInterval(0u, 7u)}; 68 | auto packet{ 69 | dcc::make_cv_access_long_write_service_packet(cv_addr, bit, position)}; 70 | 71 | // 5 or more identical packets 72 | EXPECT_CALL(_mock, writeCv(cv_addr, bit, position)).WillOnce(Return(bit)); 73 | EXPECT_CALL(_mock, serviceAck()); 74 | for (auto i{0uz}; i < 5uz; ++i) ReceiveAndExecute(packet); 75 | } 76 | 77 | TEST_F(RxTest, cv_long_write_byte_operations_mode) { 78 | // Don't write any CV which might trigger config (e.g. 1, 28, ...)! 79 | auto cv_addr{RandomInterval(30u, smath::pow(2u, 10u) - 1u)}; 80 | auto byte{RandomInterval(0u, 255u)}; 81 | 82 | EXPECT_CALL(_mock, 83 | writeCv(Matcher(cv_addr), 84 | Matcher(byte), 85 | Matcher>(_))) 86 | .WillOnce(InvokeArgument<2uz>(byte)); 87 | ReceiveAndExecuteTwice( 88 | dcc::make_cv_access_long_write_packet(_addrs.primary, cv_addr, byte)); 89 | } 90 | 91 | TEST_F( 92 | RxTest, 93 | cv_long_write_byte_operations_mode_requires_two_identical_not_back_to_back_packets) { 94 | auto cv_addr{RandomInterval(30u, smath::pow(2u, 10u) - 1u)}; 95 | auto byte{RandomInterval(0u, 255u)}; 96 | auto cv_packet{ 97 | make_cv_access_long_write_packet(_addrs.primary, cv_addr, byte)}; 98 | 99 | EXPECT_CALL(_mock, 100 | writeCv(Matcher(cv_addr), 101 | Matcher(byte), 102 | Matcher>(_))) 103 | .WillOnce(InvokeArgument<2uz>(byte)); 104 | 105 | ReceiveAndExecute(cv_packet); 106 | 107 | auto other_packet_to_different_address{ 108 | dcc::make_function_group_f4_f0_packet(42u, 0b1u)}; 109 | ReceiveAndExecute(other_packet_to_different_address); 110 | 111 | ReceiveAndExecute(cv_packet); 112 | } 113 | 114 | TEST_F( 115 | RxTest, 116 | cv_long_write_byte_operations_mode_interrupted_by_packet_to_same_address) { 117 | auto cv_addr{RandomInterval(30u, smath::pow(2u, 10u) - 1u)}; 118 | auto byte{RandomInterval(0u, 255u)}; 119 | auto cv_packet{ 120 | make_cv_access_long_write_packet(_addrs.primary, cv_addr, byte)}; 121 | 122 | EXPECT_CALL(_mock, 123 | writeCv(Matcher(cv_addr), 124 | Matcher(byte), 125 | Matcher>(_))) 126 | .Times(0); 127 | 128 | ReceiveAndExecute(cv_packet); 129 | 130 | auto other_packet_to_same_address{ 131 | make_function_group_f4_f0_packet(_addrs.primary, 0b1u)}; 132 | ReceiveAndExecute(other_packet_to_same_address); 133 | 134 | ReceiveAndExecute(cv_packet); 135 | } 136 | 137 | TEST_F(RxTest, cv_long_write_byte_service_mode) { 138 | EnterServiceMode(); 139 | 140 | // Don't write any CV which might trigger config (e.g. 1, 28, ...)! 141 | auto cv_addr{RandomInterval(30u, smath::pow(2u, 10u) - 1u)}; 142 | auto byte{RandomInterval(0u, 255u)}; 143 | auto packet{dcc::make_cv_access_long_write_service_packet(cv_addr, byte)}; 144 | 145 | // 5 or more identical packets 146 | EXPECT_CALL(_mock, writeCv(cv_addr, byte)); 147 | for (auto i{0uz}; i < 5uz; ++i) ReceiveAndExecute(packet); 148 | } 149 | -------------------------------------------------------------------------------- /examples/repl/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "command_station.hpp" 7 | #include "decoder.hpp" 8 | #include "fifo.hpp" 9 | 10 | namespace { 11 | 12 | using namespace std::chrono_literals; 13 | 14 | // Fake timer interrupt handler 15 | std::function timer_irq_handler; 16 | 17 | // Decoder task 18 | void decoder_task() { 19 | Decoder decoder; 20 | 21 | // Initializing the decoder is mandatory 22 | decoder.init(); 23 | 24 | // Register to timer interrupt 25 | timer_irq_handler = [&decoder](uint32_t ccr) { decoder.receive(ccr); }; 26 | 27 | // Continuously call execute 28 | for (;;) { 29 | std::this_thread::sleep_for(5ms); 30 | decoder.execute(); 31 | } 32 | } 33 | 34 | // Command station task 35 | void command_station_task(FiFo* fifo) { 36 | CommandStation command_station; 37 | 38 | // Initializing the command station is mandatory 39 | command_station.init({.num_preamble = DCC_TX_MIN_PREAMBLE_BITS, 40 | .bit1_duration = 58u, 41 | .bit0_duration = 100u, 42 | .flags = {.bidi = true}}); 43 | 44 | for (;;) { 45 | std::this_thread::sleep_for(1ms); 46 | 47 | // "Trigger" interrupt with latest timing 48 | auto const ccr{command_station.transmit()}; 49 | timer_irq_handler(ccr); 50 | 51 | // Read packet from FIFO 52 | if (empty(*fifo)) continue; 53 | command_station.packet(fifo->front()); 54 | fifo->pop_front(); 55 | } 56 | } 57 | 58 | // REPL task reading user input 59 | void repl_task(FiFo* fifo) { 60 | // Start with primary address 3 61 | dcc::Address addr{3u, dcc::Address::BasicLoco}; 62 | 63 | auto root{std::make_unique("dcc")}; 64 | 65 | // Change address used in commands 66 | root->Insert("address", 67 | [&](std::ostream&, dcc::Address::value_type a) { 68 | addr = a; 69 | cli::Cli::cout() << "Set address to " << a << std::endl; 70 | }, 71 | "Set address all commands are sent to", 72 | {"Address [0-16383] [default:3]"}); 73 | 74 | // Set direction and speed 75 | root->Insert("direction_speed", 76 | [&](std::ostream&, bool dir, uint8_t speed) { 77 | auto const packet{dcc::make_advanced_operations_speed_packet( 78 | addr, dir << 7u | speed)}; 79 | fifo->push_back(packet); 80 | }, 81 | "Set direction and speed", 82 | {"Direction [1 forward, 0 backward]", "Speed [0-127]"}); 83 | 84 | // Set F4-F0 85 | root->Insert("f4-f0", 86 | [&](std::ostream&, uint8_t state) { 87 | auto const packet{ 88 | dcc::make_function_group_f4_f0_packet(addr, state)}; 89 | fifo->push_back(packet); 90 | }, 91 | "Functions F4-F0", 92 | {"State [0b00000-0b11111]"}); 93 | 94 | // Set F8-F5 95 | root->Insert("f8-f5", 96 | [&](std::ostream&, uint8_t state) { 97 | auto const packet{ 98 | dcc::make_function_group_f8_f5_packet(addr, state)}; 99 | fifo->push_back(packet); 100 | }, 101 | "Functions F8-F5", 102 | {"State [0b00000-0b11111]"}); 103 | 104 | // Read CV byte 105 | root->Insert("read_cv_byte", 106 | [&](std::ostream&, uint32_t cv_addr) { 107 | auto const packet{ 108 | dcc::make_cv_access_long_verify_packet(addr, cv_addr)}; 109 | fifo->push_back(packet); 110 | }, 111 | "Read CV byte", 112 | {"CV address [0-1023]"}); 113 | 114 | // Write CV byte 115 | root->Insert("write_cv_byte", 116 | [&](std::ostream&, uint32_t cv_addr, uint8_t byte) { 117 | auto const packet{ 118 | dcc::make_cv_access_long_write_packet(addr, cv_addr, byte)}; 119 | fifo->push_back(packet); 120 | fifo->push_back(packet); 121 | }, 122 | "Write CV byte", 123 | {"CV address [0-1023]", "CV value [0-255]"}); 124 | 125 | // Read CV bit 126 | root->Insert("read_cv_bit", 127 | [&](std::ostream&, uint32_t cv_addr, bool bit, uint8_t pos) { 128 | auto const packet{dcc::make_cv_access_long_verify_packet( 129 | addr, cv_addr, bit, pos)}; 130 | fifo->push_back(packet); 131 | }, 132 | "Read CV bit", 133 | {"CV address [0-1023]", "Bit", "Bit position [0-7]"}); 134 | 135 | // Write CV bit 136 | root->Insert("write_cv_bit", 137 | [&](std::ostream&, uint32_t cv_addr, bool bit, uint8_t pos) { 138 | auto const packet{dcc::make_cv_access_long_write_packet( 139 | addr, cv_addr, bit, pos)}; 140 | fifo->push_back(packet); 141 | fifo->push_back(packet); 142 | }, 143 | "Write CV bit", 144 | {"CV address [0-1023]", "Bit", "Bit position [0-7]"}); 145 | 146 | cli::Cli cli{std::move(root)}; 147 | 148 | cli::LoopScheduler scheduler; 149 | cli::CliLocalTerminalSession session{cli, scheduler, std::cout}; 150 | session.ExitAction([&scheduler](auto&&) { scheduler.Stop(); }); 151 | scheduler.Run(); 152 | } 153 | 154 | } // namespace 155 | 156 | int main() { 157 | // Use a FIFO with internal locking to communicate between REPL and command 158 | // station 159 | FiFo fifo; 160 | 161 | std::jthread decoder_thread{decoder_task}; 162 | std::jthread command_station_thread{command_station_task, &fifo}; 163 | std::jthread repl_thread{repl_task, &fifo}; 164 | 165 | repl_thread.join(); 166 | std::exit(0); 167 | } 168 | -------------------------------------------------------------------------------- /examples/stm32/STM32H743ZITX_FLASH.ld: -------------------------------------------------------------------------------- 1 | /* 2 | ****************************************************************************** 3 | ** 4 | ** File : LinkerScript.ld 5 | ** 6 | ** Author : STM32CubeIDE 7 | ** 8 | ** Abstract : Linker script for STM32H7 series 9 | ** 2048Kbytes FLASH and 1056Kbytes RAM 10 | ** 11 | ** Set heap size, stack size and stack location according 12 | ** to application requirements. 13 | ** 14 | ** Set memory bank area and size if external memory is used. 15 | ** 16 | ** Target : STMicroelectronics STM32 17 | ** 18 | ** Distribution: The file is distributed as is, without any warranty 19 | ** of any kind. 20 | ** 21 | ***************************************************************************** 22 | ** @attention 23 | ** 24 | ** Copyright (c) 2024 STMicroelectronics. 25 | ** All rights reserved. 26 | ** 27 | ** This software is licensed under terms that can be found in the LICENSE file 28 | ** in the root directory of this software component. 29 | ** If no LICENSE file comes with this software, it is provided AS-IS. 30 | ** 31 | **************************************************************************** 32 | */ 33 | 34 | /* Entry Point */ 35 | ENTRY(Reset_Handler) 36 | 37 | /* Highest address of the user mode stack */ 38 | _estack = ORIGIN(RAM_D1) + LENGTH(RAM_D1); /* end of RAM */ 39 | /* Generate a link error if heap and stack don't fit into RAM */ 40 | _Min_Heap_Size = 0x200; /* required amount of heap */ 41 | _Min_Stack_Size = 0x400; /* required amount of stack */ 42 | 43 | /* Specify the memory areas */ 44 | MEMORY 45 | { 46 | FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K 47 | DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K 48 | RAM_D1 (xrw) : ORIGIN = 0x24000000, LENGTH = 512K 49 | RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K 50 | RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K 51 | ITCMRAM (xrw) : ORIGIN = 0x00000000, LENGTH = 64K 52 | } 53 | 54 | /* Define output sections */ 55 | SECTIONS 56 | { 57 | /* The startup code goes first into FLASH */ 58 | .isr_vector : 59 | { 60 | . = ALIGN(4); 61 | KEEP(*(.isr_vector)) /* Startup code */ 62 | . = ALIGN(4); 63 | } >FLASH 64 | 65 | /* The program code and other data goes into FLASH */ 66 | .text : 67 | { 68 | . = ALIGN(4); 69 | *(.text) /* .text sections (code) */ 70 | *(.text*) /* .text* sections (code) */ 71 | *(.glue_7) /* glue arm to thumb code */ 72 | *(.glue_7t) /* glue thumb to arm code */ 73 | *(.eh_frame) 74 | 75 | KEEP (*(.init)) 76 | KEEP (*(.fini)) 77 | 78 | . = ALIGN(4); 79 | _etext = .; /* define a global symbols at end of code */ 80 | } >FLASH 81 | 82 | /* Constant data goes into FLASH */ 83 | .rodata : 84 | { 85 | . = ALIGN(4); 86 | *(.rodata) /* .rodata sections (constants, strings, etc.) */ 87 | *(.rodata*) /* .rodata* sections (constants, strings, etc.) */ 88 | . = ALIGN(4); 89 | } >FLASH 90 | 91 | .ARM.extab (READONLY) : /* The READONLY keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ 92 | { 93 | *(.ARM.extab* .gnu.linkonce.armextab.*) 94 | } >FLASH 95 | .ARM (READONLY) : /* The READONLY keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ 96 | { 97 | __exidx_start = .; 98 | *(.ARM.exidx*) 99 | __exidx_end = .; 100 | } >FLASH 101 | 102 | .preinit_array (READONLY) : /* The READONLY keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ 103 | { 104 | PROVIDE_HIDDEN (__preinit_array_start = .); 105 | KEEP (*(.preinit_array*)) 106 | PROVIDE_HIDDEN (__preinit_array_end = .); 107 | } >FLASH 108 | 109 | .init_array (READONLY) : /* The READONLY keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ 110 | { 111 | PROVIDE_HIDDEN (__init_array_start = .); 112 | KEEP (*(SORT(.init_array.*))) 113 | KEEP (*(.init_array*)) 114 | PROVIDE_HIDDEN (__init_array_end = .); 115 | } >FLASH 116 | 117 | .fini_array (READONLY) : /* The READONLY keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */ 118 | { 119 | PROVIDE_HIDDEN (__fini_array_start = .); 120 | KEEP (*(SORT(.fini_array.*))) 121 | KEEP (*(.fini_array*)) 122 | PROVIDE_HIDDEN (__fini_array_end = .); 123 | } >FLASH 124 | 125 | /* used by the startup to initialize data */ 126 | _sidata = LOADADDR(.data); 127 | 128 | /* Initialized data sections goes into RAM, load LMA copy after code */ 129 | .data : 130 | { 131 | . = ALIGN(4); 132 | _sdata = .; /* create a global symbol at data start */ 133 | *(.data) /* .data sections */ 134 | *(.data*) /* .data* sections */ 135 | *(.RamFunc) /* .RamFunc sections */ 136 | *(.RamFunc*) /* .RamFunc* sections */ 137 | 138 | . = ALIGN(4); 139 | _edata = .; /* define a global symbol at data end */ 140 | } >RAM_D1 AT> FLASH 141 | 142 | /* Uninitialized data section */ 143 | . = ALIGN(4); 144 | .bss : 145 | { 146 | /* This is used by the startup in order to initialize the .bss section */ 147 | _sbss = .; /* define a global symbol at bss start */ 148 | __bss_start__ = _sbss; 149 | *(.bss) 150 | *(.bss*) 151 | *(COMMON) 152 | 153 | . = ALIGN(4); 154 | _ebss = .; /* define a global symbol at bss end */ 155 | __bss_end__ = _ebss; 156 | } >RAM_D1 157 | 158 | /* User_heap_stack section, used to check that there is enough RAM left */ 159 | ._user_heap_stack : 160 | { 161 | . = ALIGN(8); 162 | PROVIDE ( end = . ); 163 | PROVIDE ( _end = . ); 164 | . = . + _Min_Heap_Size; 165 | . = . + _Min_Stack_Size; 166 | . = ALIGN(8); 167 | } >RAM_D1 168 | 169 | /* Remove information from the standard libraries */ 170 | /DISCARD/ : 171 | { 172 | libc.a ( * ) 173 | libm.a ( * ) 174 | libgcc.a ( * ) 175 | } 176 | 177 | .ARM.attributes 0 : { *(.ARM.attributes) } 178 | } 179 | 180 | 181 | -------------------------------------------------------------------------------- /tests/bidi/dissector.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static_assert(std::is_trivially_copyable_v); 5 | static_assert(std::input_iterator); 6 | static_assert(std::ranges::range); 7 | static_assert(std::ranges::input_range); 8 | 9 | using namespace dcc::bidi; 10 | 11 | TEST(Dissector, channel_1_and_2) { 12 | { 13 | dcc::Packet packet{0x03u, 0x3Fu, 0x80u, 0xBCu}; 14 | Datagram<> datagram{0xA3u, 0xACu, 0x59u, 0xB1u, 0x66, 0x5Au, 0xACu, 0xACu}; 15 | Dissector dissector{datagram, packet}; 16 | std::vector expected{ 17 | app::AdrHigh{.d = 0u}, // Address 18 | app::Dyn{.d = 79u, .x = 26u}, // Temperature 19 | app::Dyn{.d = 0u, .x = 0u}}; // Speed 20 | std::vector result{}; 21 | std::ranges::copy(dissector, back_inserter(result)); 22 | EXPECT_EQ(expected, result); 23 | } 24 | 25 | { 26 | dcc::Packet packet{0x03u, 0xDEu, 0x00u, 0xDDu}; 27 | Datagram<> datagram{0xA3u, 0xACu, 0x5Au, 0xACu, 0x9Au, 0x5Au, 0xACu, 0xACu}; 28 | Dissector dissector{datagram, packet}; 29 | std::vector expected{ 30 | app::AdrHigh{.d = 0u}, // Address 31 | app::Dyn{.d = 0u, .x = 7u}, // Speed 32 | app::Dyn{.d = 0u, .x = 0u}}; // QoS 33 | std::vector result{}; 34 | std::ranges::copy(dissector, back_inserter(result)); 35 | EXPECT_EQ(expected, result); 36 | } 37 | 38 | { 39 | dcc::Packet packet{0x03u, 0xA0u, 0xA3u}; 40 | Datagram<> datagram{0x99u, 0xA5u, 0x59u, 0x2Eu, 0xD2u, 0x00u, 0x00u, 0x00u}; 41 | Dissector dissector{datagram, packet}; 42 | std::vector expected{ 43 | app::AdrLow{.d = 3u}, // Address 44 | app::Dyn{.d = 119u, .x = 46u}}; // Track voltage 45 | std::vector result{}; 46 | std::ranges::copy(dissector, back_inserter(result)); 47 | EXPECT_EQ(expected, result); 48 | } 49 | 50 | { 51 | dcc::Packet packet{0x03u, 0xDFu, 0x00u, 0xDCu}; 52 | Datagram<> datagram{0xA3u, 0xACu, 0x5Au, 0x74u, 0x5Cu, 0x5Au, 0xACu, 0xACu}; 53 | Dissector dissector{datagram, packet}; 54 | std::vector expected{ 55 | app::AdrHigh{.d = 0u}, // Address 56 | app::Dyn{.d = 19u, .x = 27u}, // Direction status byte 57 | app::Dyn{.d = 0u, .x = 0u}}; // QoS 58 | std::vector result{}; 59 | std::ranges::copy(dissector, back_inserter(result)); 60 | EXPECT_EQ(expected, result); 61 | } 62 | 63 | { 64 | dcc::Packet packet{0xC5u, 0x39u, 0xB0u, 0x4Cu}; 65 | Datagram<> datagram{0x9Cu, 0xA6u, 0x5Au, 0xA3u, 0xACu, 0x00u, 0x00u, 0x00u}; 66 | Dissector dissector{datagram, packet}; 67 | std::vector expected{ 68 | app::AdrHigh{.d = 133u}, // Address 69 | app::Dyn{.d = 4u, .x = 0u}}; // Speed 70 | std::vector result{}; 71 | std::ranges::copy(dissector, back_inserter(result)); 72 | EXPECT_EQ(expected, result); 73 | } 74 | 75 | { 76 | dcc::Packet packet{0xC5u, 0x39u, 0xE4u, 0x00u, 0x00u, 0x18u}; 77 | Datagram<> datagram{0x99u, 0x3Au, 0xACu, 0xA5u, 0x00u, 0x00u, 0x00u, 0x00u}; 78 | Dissector dissector{datagram, packet}; 79 | std::vector expected{ 80 | app::AdrLow{.d = 57u}, // Address 81 | app::Pom{.d = 3u}}; // Pom 82 | std::vector result{}; 83 | std::ranges::copy(dissector, back_inserter(result)); 84 | EXPECT_EQ(expected, result); 85 | } 86 | 87 | { 88 | dcc::Packet packet{0x03u, 0x3Fu, 0x80u, 0xBCu}; 89 | Datagram<> datagram{0x99u, 0xA5u, 0x0Fu, 0x0Fu, 0x00u, 0x00u, 0x00u, 0x00u}; 90 | Dissector dissector{datagram, packet}; 91 | std::vector expected{app::AdrLow{.d = 3u}, // Address 92 | acks[0uz]}; // ACK 93 | std::vector result{}; 94 | std::ranges::copy(dissector, back_inserter(result)); 95 | EXPECT_EQ(expected, result); 96 | } 97 | 98 | { 99 | dcc::Packet packet{0x03u, 0x3Fu, 0xB6u, 0x8Au}; 100 | Datagram<> datagram{0xA3u, 0xACu, 0xACu, 0xA5u, 0x0Fu, 0x0Fu, 0x00u, 0x00u}; 101 | Dissector dissector{datagram, packet}; 102 | std::vector expected{ 103 | app::AdrHigh{.d = 0u}, // Address 104 | app::Pom{.d = 3u}, // Pom 105 | acks[0uz]}; // ACK 106 | std::vector result{}; 107 | std::ranges::copy(dissector, back_inserter(result)); 108 | EXPECT_EQ(expected, result); 109 | } 110 | } 111 | 112 | TEST(Dissector, channel_1) { 113 | { 114 | dcc::Packet packet{0x03u, 0xA0u, 0xA3u}; 115 | Datagram<> datagram{0x99u, 0xA5u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u}; 116 | Dissector dissector{datagram, packet}; 117 | std::vector expected{ 118 | app::AdrLow{.d = 3u}}; // Address 119 | std::vector result{}; 120 | std::ranges::copy(dissector, back_inserter(result)); 121 | EXPECT_EQ(expected, result); 122 | } 123 | } 124 | 125 | TEST(Dissector, channel_2) { 126 | { 127 | dcc::Packet packet{0x03u, 0x3Fu, 0x80u, 0xBCu}; 128 | Datagram<> datagram{0x00u, 0x00u, 0x59u, 0xB1u, 0x66, 0x5Au, 0xACu, 0xACu}; 129 | Dissector dissector{datagram, packet}; 130 | std::vector expected{ 131 | app::Dyn{.d = 79u, .x = 26u}, // Temperature 132 | app::Dyn{.d = 0u, .x = 0u}}; // Speed 133 | std::vector result{}; 134 | std::ranges::copy(dissector, back_inserter(result)); 135 | EXPECT_EQ(expected, result); 136 | } 137 | } 138 | 139 | TEST(Dissector, invalid) { 140 | { 141 | dcc::Packet packet{0x03u, 0x3Fu, 0x80u, 0xBCu}; 142 | Datagram<> datagram{0x00u, 0x00u, 0xD9u, 0xB1u, 0x66, 0x5Au, 0xACu, 0xACu}; 143 | Dissector dissector{datagram, packet}; 144 | std::vector expected{}; 145 | std::vector result{}; 146 | std::ranges::copy(dissector, back_inserter(result)); 147 | EXPECT_EQ(expected, result); 148 | } 149 | } 150 | 151 | TEST(Dissector, unknown_id) { 152 | { 153 | dcc::Packet packet{0x03u, 0x3Fu, 0x80u, 0xBCu}; 154 | Datagram<> datagram{0x2Du, 0xCAu, 0x59u, 0x96u, 0x66u, 0x5Au, 0xACu, 0xACu}; 155 | Dissector dissector{datagram, packet}; 156 | EXPECT_EQ(cbegin(dissector), cend(dissector)); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /include/dcc/bidi/datagram.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// BiDi datagram 6 | /// 7 | /// \file dcc/bidi/datagram.hpp 8 | /// \author Vincent Hamp 9 | /// \date 12/02/2023 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace dcc::bidi { 20 | 21 | namespace detail { 22 | 23 | inline constexpr std::array encode_lut{ 24 | 0xACu, 0xAAu, 0xA9u, 0xA5u, 0xA3u, 0xA6u, 0x9Cu, 0x9Au, 0x99u, 0x95u, 0x93u, 25 | 0x96u, 0x8Eu, 0x8Du, 0x8Bu, 0xB1u, 0xB2u, 0xB4u, 0xB8u, 0x74u, 0x72u, 0x6Cu, 26 | 0x6Au, 0x69u, 0x65u, 0x63u, 0x66u, 0x5Cu, 0x5Au, 0x59u, 0x55u, 0x53u, 0x56u, 27 | 0x4Eu, 0x4Du, 0x4Bu, 0x47u, 0x71u, 0xE8u, 0xE4u, 0xE2u, 0xD1u, 0xC9u, 0xC5u, 28 | 0xD8u, 0xD4u, 0xD2u, 0xCAu, 0xC6u, 0xCCu, 0x78u, 0x17u, 0x1Bu, 0x1Du, 0x1Eu, 29 | 0x2Eu, 0x36u, 0x3Au, 0x27u, 0x2Bu, 0x2Du, 0x35u, 0x39u, 0x33u}; 30 | 31 | inline constexpr std::array decode_lut{ 32 | 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 33 | 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 34 | 0x00u, 0x33u, 0x00u, 0x00u, 0x00u, 0x34u, 0x00u, 0x35u, 0x36u, 0x00u, 0x00u, 35 | 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x3Au, 0x00u, 0x00u, 0x00u, 0x3Bu, 36 | 0x00u, 0x3Cu, 0x37u, 0x00u, 0x00u, 0x00u, 0x00u, 0x3Fu, 0x00u, 0x3Du, 0x38u, 37 | 0x00u, 0x00u, 0x3Eu, 0x39u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 38 | 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x24u, 0x00u, 0x00u, 0x00u, 0x23u, 0x00u, 39 | 0x22u, 0x21u, 0x00u, 0x00u, 0x00u, 0x00u, 0x1Fu, 0x00u, 0x1Eu, 0x20u, 0x00u, 40 | 0x00u, 0x1Du, 0x1Cu, 0x00u, 0x1Bu, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 41 | 0x19u, 0x00u, 0x18u, 0x1Au, 0x00u, 0x00u, 0x17u, 0x16u, 0x00u, 0x15u, 0x00u, 42 | 0x00u, 0x00u, 0x00u, 0x25u, 0x14u, 0x00u, 0x13u, 0x00u, 0x00u, 0x00u, 0x32u, 43 | 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 44 | 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x0Eu, 0x00u, 0x0Du, 0x0Cu, 45 | 0x00u, 0x00u, 0x00u, 0x00u, 0x0Au, 0x00u, 0x09u, 0x0Bu, 0x00u, 0x00u, 0x08u, 46 | 0x07u, 0x00u, 0x06u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x04u, 0x00u, 47 | 0x03u, 0x05u, 0x00u, 0x00u, 0x02u, 0x01u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 48 | 0x00u, 0x0Fu, 0x10u, 0x00u, 0x11u, 0x00u, 0x00u, 0x00u, 0x12u, 0x00u, 0x00u, 49 | 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x2Bu, 50 | 0x30u, 0x00u, 0x00u, 0x2Au, 0x2Fu, 0x00u, 0x31u, 0x00u, 0x00u, 0x00u, 0x00u, 51 | 0x29u, 0x2Eu, 0x00u, 0x2Du, 0x00u, 0x00u, 0x00u, 0x2Cu, 0x00u, 0x00u, 0x00u, 52 | 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x28u, 0x00u, 0x27u, 0x00u, 0x00u, 53 | 0x00u, 0x26u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 54 | 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 55 | 0x00u, 0x00u, 0x00u}; 56 | 57 | } // namespace detail 58 | 59 | /// Enumeration to specify datagram bits 60 | enum struct Bits { _12 = 12uz, _18 = 18uz, _24 = 24uz, _36 = 36uz, _48 = 48uz }; 61 | 62 | /// Convert datagram bits to size 63 | /// 64 | /// \tparam I Datagram bits 65 | template 66 | inline constexpr auto datagram_size{(std::to_underlying(I) + 5uz) / 6uz}; 67 | 68 | template> 69 | using Datagram = std::array; 70 | 71 | /// Make datagram with ID 72 | /// 73 | /// Currently supported datagrams are either 74 | /// 12bit: ID[3-0]D[7-6]+D[5-0] 75 | /// 18bit: ID[3-0]D[13-12]+D[11-6]+D[5-0] 76 | /// 24bit: ID[3-0]D[19-18]+D[17-12]+D[11-6]+D[5-0] 77 | /// 36bit: ID[3-0]D[31-30]+D[29-24]+D[23-18]+D[17-12]+D[11-6]+D[5-0] 78 | /// 48bit: ID[3-0]D[43-42]+D[41-36]+D[35-30]+D[29-24]+D[23-18]+D[17-12]+D[11-6] 79 | /// +D[5-0] 80 | /// 81 | /// \tparam I Datagram size in bits 82 | /// \tparam T Type of data 83 | /// \param id ID 84 | /// \param data Data 85 | /// \return 12, 18, 24, 36 or 48bit datagram 86 | template 87 | constexpr auto make_datagram(uint8_t id, T data) { 88 | Datagram> datagram{}; 89 | // Data must be smaller than the number of bits (-4 for ID) 90 | assert(data < smath::pow(2u, datagram_size * 6uz - 4uz)); 91 | for (auto i{size(datagram) - 1u}; i > 0u; --i) { 92 | datagram[i] = data & 0x3Fu; 93 | data >>= 6u; 94 | } 95 | datagram.front() = static_cast(id << 2u) | (data & 0x03u); 96 | return datagram; 97 | } 98 | 99 | /// Make datagram without ID 100 | /// 101 | /// \tparam I Datagram size in bits 102 | /// \tparam T Type of data 103 | /// \param data Data 104 | /// \return 12, 18, 24, 36 or 48bit datagram 105 | template 106 | constexpr auto make_datagram(T data) { 107 | Datagram> datagram{}; 108 | // Data must be smaller than the number of bits 109 | assert(data < smath::pow(2u, datagram_size * 6uz)); 110 | for (auto i{size(datagram)}; i-- > 0u;) { 111 | datagram[i] = data & 0x3Fu; 112 | data >>= 6u; 113 | } 114 | return datagram; 115 | } 116 | 117 | /// Encode datagram 118 | /// 119 | /// \tparam I Size of array 120 | /// \param datagram Datagram 121 | /// \return Encoded datagram 122 | template 123 | constexpr auto encode_datagram(Datagram const& datagram) { 124 | Datagram encoded_datagram{}; 125 | std::ranges::transform(datagram, begin(encoded_datagram), [](uint8_t b) { 126 | return detail::encode_lut[b]; 127 | }); 128 | return encoded_datagram; 129 | } 130 | 131 | /// Decode datagram 132 | /// 133 | /// \tparam I Size of array 134 | /// \param encoded_datagram Encoded datagram 135 | /// \return Datagram 136 | template 137 | constexpr auto decode_datagram(Datagram const& encoded_datagram) { 138 | Datagram datagram{}; 139 | std::ranges::transform(encoded_datagram, begin(datagram), [](uint8_t b) { 140 | return detail::decode_lut[b]; 141 | }); 142 | return datagram; 143 | } 144 | 145 | /// Make data 146 | /// 147 | /// \tparam I Size of array 148 | /// \param datagram Datagram 149 | /// \return Data 150 | constexpr auto make_data(std::span datagram) { 151 | uint64_t data{}; 152 | for (auto const b : datagram) { 153 | data <<= 6u; 154 | data |= b & 0x3Fu; 155 | } 156 | // Data must be smaller than the number of bits 157 | assert(data < smath::pow(2u, size(datagram) * 6uz)); 158 | return data; 159 | } 160 | 161 | } // namespace dcc::bidi 162 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25 FATAL_ERROR) 2 | include(FetchContent) 3 | 4 | if(ESP_PLATFORM) 5 | # >5.3.0 requires esp_driver_rmt 6 | if(IDF_VERSION_MAJOR GREATER 5 OR (IDF_VERSION_MAJOR EQUAL 5 7 | AND IDF_VERSION_MINOR GREATER_EQUAL 3)) 8 | set(ESP_DRIVER_RMT_COMPONENT esp_driver_rmt) 9 | endif() 10 | idf_component_register( 11 | SRCS 12 | src/rmt_dcc_encoder.c 13 | INCLUDE_DIRS 14 | include 15 | REQUIRES 16 | driver 17 | ${ESP_DRIVER_RMT_COMPONENT}) 18 | 19 | # Change OUTPUT_NAME from DCC to lib__idf_DCC to avoid conflicts 20 | set_target_properties(${COMPONENT_LIB} PROPERTIES PREFIX "") 21 | set_target_properties( 22 | ${COMPONENT_LIB} PROPERTIES OUTPUT_NAME 23 | ${CMAKE_STATIC_LIBRARY_PREFIX}${COMPONENT_LIB}) 24 | 25 | target_link_libraries(${COMPONENT_LIB} PUBLIC DCC) 26 | endif() 27 | 28 | FetchContent_Declare( 29 | CMakeModules 30 | GIT_REPOSITORY "https://github.com/ZIMO-Elektronik/CMakeModules" 31 | GIT_TAG v0.10.0 32 | SOURCE_DIR ${CMAKE_BINARY_DIR}/CMakeModules) 33 | FetchContent_MakeAvailable(CMakeModules) 34 | 35 | version_from_git() 36 | project( 37 | DCC 38 | VERSION ${VERSION_FROM_GIT} 39 | LANGUAGES ASM C CXX) 40 | 41 | option(DCC_STANDARD_COMPLIANCE "Standard compliance" OFF) 42 | set(DCC_MANUFACTURER_ID 43 | 145u 44 | CACHE STRING "Manufacturer ID") 45 | set(DCC_MAX_PACKET_SIZE 46 | 18u 47 | CACHE STRING "Maximum size of a packet in bytes") 48 | set(DCC_RX_LOGON_DID_CV_ADDRESS 49 | 249u 50 | CACHE STRING "First CV address of decoder ID") 51 | set(DCC_RX_LOGON_CID_CV_ADDRESS 52 | 65296u 53 | CACHE STRING "First CV address of last CID") 54 | set(DCC_RX_LOGON_SID_CV_ADDRESS 55 | 65298u 56 | CACHE STRING "First CV address of last SID") 57 | set(DCC_RX_LOGON_ADDRESS_CV_ADDRESS 58 | 65299u 59 | CACHE STRING "First CV address of last logon address") 60 | set(DCC_RX_MIN_PREAMBLE_BITS 61 | 10u 62 | CACHE STRING "Minimum number of preamble bits of decoder") 63 | set(DCC_RX_MIN_BIT_1_TIMING 64 | 52u 65 | CACHE STRING "Minimum duration of a 1 bit of decoder") 66 | set(DCC_RX_MAX_BIT_1_TIMING 67 | 64u 68 | CACHE STRING "Maximum duration of a 1 bit of decoder") 69 | set(DCC_RX_MIN_BIT_0_TIMING 70 | 90u 71 | CACHE STRING "Minimum duration of a 0 bit of decoder") 72 | set(DCC_RX_MAX_BIT_0_TIMING 73 | 119u 74 | CACHE STRING "Maximum duration of a 0 bit of decoder") 75 | set(DCC_RX_DEQUE_SIZE 76 | 31u 77 | CACHE STRING "Size of the receiver deque of decoder") 78 | set(DCC_RX_BIDI_DEQUE_SIZE 79 | 7u 80 | CACHE STRING "Size of the sender deque of decoder") 81 | set(DCC_TX_MIN_PREAMBLE_BITS 82 | 17u 83 | CACHE STRING "Minimum number of preamble bits of command station") 84 | set(DCC_TX_MAX_PREAMBLE_BITS 85 | 30u 86 | CACHE STRING "Maximum number of preamble bits of command station") 87 | set(DCC_TX_MIN_BIT_1_TIMING 88 | 56u 89 | CACHE STRING "Minimum duration of a 1 bit of command station") 90 | set(DCC_TX_MAX_BIT_1_TIMING 91 | 60u 92 | CACHE STRING "Maximum duration of a 1 bit of command station") 93 | set(DCC_TX_MIN_BIT_0_TIMING 94 | 97u 95 | CACHE STRING "Minimum duration of a 0 bit of command station") 96 | set(DCC_TX_MAX_BIT_0_TIMING 97 | 114u 98 | CACHE STRING "Maximum duration of a 0 bit of command station") 99 | set(DCC_TX_MIN_BIDI_BIT_TIMING 100 | 57u 101 | CACHE STRING "Minimum duration of a BiDi bit of command station") 102 | set(DCC_TX_MAX_BIDI_BIT_TIMING 103 | 61u 104 | CACHE STRING "Maximum duration of a BiDi bit of command station") 105 | set(DCC_TX_DEQUE_SIZE 106 | 3u 107 | CACHE STRING "Size of the transmitter deque of command station") 108 | 109 | add_library(DCC INTERFACE) 110 | add_library(DCC::DCC ALIAS DCC) 111 | 112 | target_compile_features(DCC INTERFACE cxx_std_23) 113 | 114 | target_compile_definitions( 115 | DCC 116 | INTERFACE DCC_STANDARD_COMPLIANCE=$ 117 | DCC_MANUFACTURER_ID=${DCC_MANUFACTURER_ID} 118 | DCC_MAX_PACKET_SIZE=${DCC_MAX_PACKET_SIZE} 119 | DCC_RX_LOGON_DID_CV_ADDRESS=${DCC_RX_LOGON_DID_CV_ADDRESS} 120 | DCC_RX_LOGON_CID_CV_ADDRESS=${DCC_RX_LOGON_CID_CV_ADDRESS} 121 | DCC_RX_LOGON_SID_CV_ADDRESS=${DCC_RX_LOGON_SID_CV_ADDRESS} 122 | DCC_RX_LOGON_ADDRESS_CV_ADDRESS=${DCC_RX_LOGON_ADDRESS_CV_ADDRESS} 123 | DCC_RX_MIN_PREAMBLE_BITS=${DCC_RX_MIN_PREAMBLE_BITS} 124 | DCC_RX_MIN_BIT_1_TIMING=${DCC_RX_MIN_BIT_1_TIMING} 125 | DCC_RX_MAX_BIT_1_TIMING=${DCC_RX_MAX_BIT_1_TIMING} 126 | DCC_RX_MIN_BIT_0_TIMING=${DCC_RX_MIN_BIT_0_TIMING} 127 | DCC_RX_MAX_BIT_0_TIMING=${DCC_RX_MAX_BIT_0_TIMING} 128 | DCC_RX_DEQUE_SIZE=${DCC_RX_DEQUE_SIZE} 129 | DCC_RX_BIDI_DEQUE_SIZE=${DCC_RX_BIDI_DEQUE_SIZE} 130 | DCC_TX_MIN_PREAMBLE_BITS=${DCC_TX_MIN_PREAMBLE_BITS} 131 | DCC_TX_MAX_PREAMBLE_BITS=${DCC_TX_MAX_PREAMBLE_BITS} 132 | DCC_TX_MIN_BIT_1_TIMING=${DCC_TX_MIN_BIT_1_TIMING} 133 | DCC_TX_MAX_BIT_1_TIMING=${DCC_TX_MAX_BIT_1_TIMING} 134 | DCC_TX_MIN_BIT_0_TIMING=${DCC_TX_MIN_BIT_0_TIMING} 135 | DCC_TX_MAX_BIT_0_TIMING=${DCC_TX_MAX_BIT_0_TIMING} 136 | DCC_TX_MIN_BIDI_BIT_TIMING=${DCC_TX_MIN_BIDI_BIT_TIMING} 137 | DCC_TX_MAX_BIDI_BIT_TIMING=${DCC_TX_MAX_BIDI_BIT_TIMING} 138 | DCC_TX_DEQUE_SIZE=${DCC_TX_DEQUE_SIZE}) 139 | 140 | # https://github.com/espressif/esp-idf/issues/17773 141 | if(PROJECT_IS_TOP_LEVEL AND NOT ESP_PLATFORM) 142 | target_include_directories(DCC INTERFACE include) 143 | target_common_warnings(DCC INTERFACE) 144 | target_common_errors(DCC INTERFACE) 145 | else() 146 | target_include_directories(DCC SYSTEM INTERFACE include) 147 | endif() 148 | 149 | if(NOT TARGET static_math) 150 | cpmaddpackage( 151 | NAME 152 | static_math 153 | GITHUB_REPOSITORY 154 | "Morwenn/static_math" 155 | GIT_TAG 156 | master 157 | SYSTEM 158 | YES 159 | OPTIONS 160 | "STATIC_MATH_BUILD_TESTS OFF" 161 | "CMAKE_POLICY_VERSION_MINIMUM 3.5") 162 | endif() 163 | 164 | if(NOT TARGET ZTL::ZTL) 165 | cpmaddpackage("gh:ZIMO-Elektronik/ZTL@0.23.1") 166 | endif() 167 | 168 | target_link_libraries(DCC INTERFACE static_math ZTL::ZTL) 169 | 170 | if(PROJECT_IS_TOP_LEVEL) 171 | include(CTest) 172 | add_subdirectory(examples) 173 | file( 174 | DOWNLOAD 175 | "https://github.com/ZIMO-Elektronik/.github/raw/master/data/.clang-format" 176 | ${CMAKE_CURRENT_LIST_DIR}/.clang-format) 177 | file(GLOB_RECURSE SRC examples/*.[ch]pp include/*.[ch]pp src/*.[ch]pp 178 | tests/*.[ch]pp) 179 | add_clang_format_target(DCCFormat OPTIONS -i FILES ${SRC}) 180 | endif() 181 | 182 | if(BUILD_TESTING 183 | AND PROJECT_IS_TOP_LEVEL 184 | AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME) 185 | add_subdirectory(tests) 186 | endif() 187 | -------------------------------------------------------------------------------- /include/dcc/tx/crtp_base.hpp: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /// Transmit base 6 | /// 7 | /// \file dcc/tx/crtp_base.hpp 8 | /// \author Vincent Hamp 9 | /// \date 12/06/2022 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include "../bidi/datagram.hpp" 17 | #include "../bidi/timing.hpp" 18 | #include "command_station.hpp" 19 | #include "config.hpp" 20 | #include "timings.hpp" 21 | #include "timings_adapter.hpp" 22 | 23 | namespace dcc::tx { 24 | 25 | /// CRTP base for transmitting DCC 26 | /// 27 | /// \tparam T Type to downcast to 28 | /// \tparam D Deque value type 29 | template 30 | requires(std::same_as || std::same_as) 31 | struct CrtpBase { 32 | friend T; 33 | 34 | using value_type = 35 | std::conditional_t, TimingsAdapter, Timings>; 36 | 37 | /// Initialize 38 | /// 39 | /// \param cfg Configuration 40 | /// \param packet Packet to send as soon as deque is empty 41 | void init(Config cfg = {}, Packet const& packet = make_idle_packet()) { 42 | assert(cfg.num_preamble >= DCC_TX_MIN_PREAMBLE_BITS && // 43 | cfg.num_preamble <= DCC_TX_MAX_PREAMBLE_BITS && // 44 | cfg.bit1_duration >= Bit1Min && cfg.bit1_duration <= Bit1Max && // 45 | cfg.bit0_duration >= Bit0Min && cfg.bit0_duration <= Bit0Max); // 46 | _cfg = cfg; 47 | if constexpr (std::same_as) 48 | _idle_packet = TimingsAdapter{packet, _cfg}; 49 | else if constexpr (std::same_as) 50 | _idle_packet = bytes2timings(packet, _cfg); 51 | _first = begin(_idle_packet); 52 | _last = cend(_idle_packet); 53 | } 54 | 55 | /// Transmit packet 56 | /// 57 | /// \param packet Packet 58 | /// \retval true Packet added to deque 59 | /// \retval false Packet not added to deque 60 | bool packet(Packet const& packet) { 61 | return bytes({cbegin(packet), std::size(packet)}); 62 | } 63 | 64 | /// Transmit bytes 65 | /// 66 | /// \param bytes Bytes containing DCC packet 67 | /// \retval true Bytes added to deque 68 | /// \retval false Bytes not added to deque 69 | bool bytes(std::span bytes) { 70 | if (full(_deque)) return false; 71 | assert(std::size(bytes) <= DCC_MAX_PACKET_SIZE); 72 | pushBack(bytes); 73 | return true; 74 | } 75 | 76 | /// Get next bit duration to transmit in µs 77 | /// 78 | /// \return Bit duration in µs 79 | Timings::value_type transmit() { 80 | // Packet timings 81 | if (_first != _last) return packetTiming(); 82 | // Packet end 83 | else if constexpr (requires(T t) { 84 | { t.packetEnd() }; 85 | }) 86 | if (!_bidi_count) impl().packetEnd(); 87 | 88 | // BiDi timings 89 | if (_cfg.flags.bidi && _bidi_count <= 4uz) return biDiTiming(); 90 | else _bidi_count = 0uz; 91 | 92 | // Deque is empty, transmit idle packet 93 | if (empty(_deque)) { 94 | _first = begin(_idle_packet); 95 | _last = cend(_idle_packet); 96 | } 97 | // Deque contains packet, transmit it 98 | else { 99 | _first = begin(_deque.front()); 100 | _last = cend(_deque.front()); 101 | /// \warning 102 | /// Careful! This only works because of the design of ztl::inplace_deque. 103 | /// The element currently pointed to will stay valid until the next call 104 | /// of pop_front(). 105 | _deque.pop_front(); 106 | } 107 | return packetTiming(); 108 | } 109 | 110 | /// Get deque size 111 | /// 112 | /// \return Deque size 113 | constexpr auto size() const { return std::size(_deque); } 114 | 115 | /// Get deque capacity 116 | /// 117 | /// \return Deque capacity 118 | constexpr auto capacity() const { return DCC_TX_DEQUE_SIZE; } 119 | 120 | private: 121 | constexpr CrtpBase() = default; 122 | auto& impl() { return static_cast(*this); } 123 | auto const& impl() const { return static_cast(*this); } 124 | 125 | /// Packet timing 126 | /// 127 | /// \return Next timings from current packet 128 | Timings::value_type packetTiming() { 129 | toggleTrackOutputs(); 130 | auto const retval{*_first}; 131 | ++_first; 132 | return retval; 133 | } 134 | 135 | /// BiDi timing 136 | /// 137 | /// \return Next BiDi timing 138 | Timings::value_type biDiTiming() { 139 | switch (_bidi_count++) { 140 | // Send half a 1 bit 141 | case 0uz: 142 | toggleTrackOutputs(); 143 | return static_cast(bidi::Timing::TCS); 144 | 145 | // Cutout start 146 | case 1uz: 147 | toggleTrackOutputs(); 148 | if constexpr (requires(T t) { 149 | { t.biDiStart() }; 150 | }) 151 | impl().biDiStart(); 152 | return static_cast(bidi::Timing::TTS1 - 153 | bidi::Timing::TCS); 154 | 155 | // Channel 1 start 156 | case 2uz: 157 | if constexpr (requires(T t) { 158 | { t.biDiChannel1() }; 159 | }) 160 | impl().biDiChannel1(); 161 | return static_cast(bidi::Timing::TTS2 - 162 | bidi::Timing::TTS1); 163 | 164 | // Channel 2 start 165 | case 3uz: 166 | if constexpr (requires(T t) { 167 | { t.biDiChannel2() }; 168 | }) 169 | impl().biDiChannel2(); 170 | return static_cast(bidi::Timing::TTC2 - 171 | bidi::Timing::TTS2); 172 | 173 | // Cutout end 174 | default: 175 | if constexpr (requires(T t) { 176 | { t.biDiEnd() }; 177 | }) 178 | impl().biDiEnd(); 179 | return static_cast(bidi::Timing::TCE - 180 | bidi::Timing::TTC2); 181 | } 182 | } 183 | 184 | /// Add packet or timings to deque 185 | /// 186 | /// \param bytes Bytes containing DCC packet 187 | void pushBack(std::span bytes) { 188 | if constexpr (std::same_as) _deque.push_back({bytes, _cfg}); 189 | else if constexpr (std::same_as) 190 | _deque.push_back(bytes2timings(bytes, _cfg)); 191 | } 192 | 193 | /// Toggle track outputs 194 | void toggleTrackOutputs() { 195 | if constexpr (requires(T t, bool N, bool P) { 196 | { t.trackOutputs(N, P) }; 197 | }) { 198 | // By default the phase is "positive", so P > N for the first half bit. 199 | impl().trackOutputs(_polarity, !_polarity); 200 | _polarity = !_polarity; 201 | } 202 | } 203 | 204 | /// Deque 205 | ztl::inplace_deque _deque{}; 206 | 207 | /// Idle packet 208 | value_type _idle_packet{}; 209 | 210 | /// Iterators 211 | decltype(std::begin(_idle_packet)) _first{std::begin(_idle_packet)}; 212 | decltype(std::cend(_idle_packet)) _last{std::cend(_idle_packet)}; 213 | 214 | size_t _bidi_count{}; ///< Count BiDi timings 215 | Config _cfg{}; ///< Configuration 216 | bool _polarity{}; ///< Track polarity 217 | }; 218 | 219 | } // namespace dcc::tx 220 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: terser LLVM 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignArrayOfStructures: None 7 | AlignConsecutiveAssignments: 8 | Enabled: false 9 | AcrossEmptyLines: false 10 | AcrossComments: false 11 | AlignCompound: false 12 | AlignFunctionPointers: false 13 | PadOperators: true 14 | AlignConsecutiveBitFields: 15 | Enabled: false 16 | AcrossEmptyLines: false 17 | AcrossComments: false 18 | AlignCompound: false 19 | AlignFunctionPointers: false 20 | PadOperators: false 21 | AlignConsecutiveDeclarations: 22 | Enabled: false 23 | AcrossEmptyLines: false 24 | AcrossComments: false 25 | AlignCompound: false 26 | AlignFunctionPointers: false 27 | PadOperators: false 28 | AlignConsecutiveMacros: 29 | Enabled: false 30 | AcrossEmptyLines: false 31 | AcrossComments: false 32 | AlignCompound: false 33 | AlignFunctionPointers: false 34 | PadOperators: false 35 | AlignConsecutiveShortCaseStatements: 36 | Enabled: false 37 | AcrossEmptyLines: false 38 | AcrossComments: false 39 | AlignCaseColons: false 40 | AlignEscapedNewlines: Right 41 | AlignOperands: Align 42 | AlignTrailingComments: 43 | Kind: Always 44 | OverEmptyLines: 0 45 | AllowAllArgumentsOnNextLine: true 46 | AllowAllParametersOfDeclarationOnNextLine: true 47 | AllowBreakBeforeNoexceptSpecifier: Never 48 | AllowShortBlocksOnASingleLine: Always 49 | AllowShortCaseLabelsOnASingleLine: true 50 | AllowShortCompoundRequirementOnASingleLine: true 51 | AllowShortEnumsOnASingleLine: true 52 | AllowShortFunctionsOnASingleLine: All 53 | AllowShortIfStatementsOnASingleLine: AllIfsAndElse 54 | AllowShortLambdasOnASingleLine: All 55 | AllowShortLoopsOnASingleLine: true 56 | AlwaysBreakAfterDefinitionReturnType: None 57 | AlwaysBreakAfterReturnType: None 58 | AlwaysBreakBeforeMultilineStrings: false 59 | AlwaysBreakTemplateDeclarations: Yes 60 | AttributeMacros: 61 | - __capability 62 | BinPackArguments: false 63 | BinPackParameters: false 64 | BitFieldColonSpacing: Both 65 | BraceWrapping: 66 | AfterCaseLabel: false 67 | AfterClass: false 68 | AfterControlStatement: Never 69 | AfterEnum: false 70 | AfterExternBlock: false 71 | AfterFunction: false 72 | AfterNamespace: false 73 | AfterObjCDeclaration: false 74 | AfterStruct: false 75 | AfterUnion: false 76 | BeforeCatch: false 77 | BeforeElse: false 78 | BeforeLambdaBody: false 79 | BeforeWhile: false 80 | IndentBraces: false 81 | SplitEmptyFunction: true 82 | SplitEmptyRecord: true 83 | SplitEmptyNamespace: true 84 | BreakAdjacentStringLiterals: true 85 | BreakAfterAttributes: Never 86 | BreakAfterJavaFieldAnnotations: false 87 | BreakArrays: true 88 | BreakBeforeBinaryOperators: None 89 | BreakBeforeConceptDeclarations: Always 90 | BreakBeforeBraces: Attach 91 | BreakBeforeInlineASMColon: OnlyMultiline 92 | BreakBeforeTernaryOperators: true 93 | BreakConstructorInitializers: BeforeColon 94 | BreakInheritanceList: BeforeColon 95 | BreakStringLiterals: true 96 | ColumnLimit: 80 97 | CommentPragmas: '^ IWYU pragma:' 98 | CompactNamespaces: false 99 | ConstructorInitializerIndentWidth: 2 100 | ContinuationIndentWidth: 2 101 | Cpp11BracedListStyle: true 102 | DerivePointerAlignment: false 103 | DisableFormat: false 104 | EmptyLineAfterAccessModifier: Never 105 | EmptyLineBeforeAccessModifier: LogicalBlock 106 | ExperimentalAutoDetectBinPacking: false 107 | FixNamespaceComments: true 108 | ForEachMacros: 109 | - foreach 110 | - Q_FOREACH 111 | - BOOST_FOREACH 112 | IfMacros: 113 | - KJ_IF_MAYBE 114 | IncludeBlocks: Preserve 115 | IncludeCategories: 116 | - Regex: '^<.*\.h>' 117 | Priority: 1 118 | SortPriority: 0 119 | CaseSensitive: false 120 | - Regex: '^<.*' 121 | Priority: 2 122 | SortPriority: 0 123 | CaseSensitive: false 124 | - Regex: '.*' 125 | Priority: 3 126 | SortPriority: 0 127 | CaseSensitive: false 128 | IncludeIsMainRegex: '([-_](test|unittest))?$' 129 | IncludeIsMainSourceRegex: '' 130 | IndentAccessModifiers: false 131 | IndentCaseBlocks: false 132 | IndentCaseLabels: true 133 | IndentExternBlock: AfterExternBlock 134 | IndentGotoLabels: true 135 | IndentPPDirectives: AfterHash 136 | IndentRequiresClause: false 137 | IndentWidth: 2 138 | IndentWrappedFunctionNames: false 139 | InsertBraces: false 140 | InsertNewlineAtEOF: true 141 | InsertTrailingCommas: None 142 | IntegerLiteralSeparator: 143 | Binary: 0 144 | BinaryMinDigits: 0 145 | Decimal: 0 146 | DecimalMinDigits: 0 147 | Hex: 0 148 | HexMinDigits: 0 149 | JavaScriptQuotes: Leave 150 | JavaScriptWrapImports: true 151 | KeepEmptyLinesAtTheStartOfBlocks: true 152 | KeepEmptyLinesAtEOF: false 153 | LambdaBodyIndentation: Signature 154 | LineEnding: LF 155 | MacroBlockBegin: '' 156 | MacroBlockEnd: '' 157 | MaxEmptyLinesToKeep: 1 158 | NamespaceIndentation: None 159 | ObjCBinPackProtocolList: Auto 160 | ObjCBlockIndentWidth: 2 161 | ObjCBreakBeforeNestedBlockParam: true 162 | ObjCSpaceAfterProperty: false 163 | ObjCSpaceBeforeProtocolList: true 164 | PackConstructorInitializers: BinPack 165 | PenaltyBreakAssignment: 2 166 | PenaltyBreakBeforeFirstCallParameter: 19 167 | PenaltyBreakComment: 300 168 | PenaltyBreakFirstLessLess: 120 169 | PenaltyBreakOpenParenthesis: 0 170 | PenaltyBreakScopeResolution: 500 171 | PenaltyBreakString: 1000 172 | PenaltyBreakTemplateDeclaration: 10 173 | PenaltyExcessCharacter: 1000000 174 | PenaltyIndentedWhitespace: 0 175 | PenaltyReturnTypeOnItsOwnLine: 60 176 | PointerAlignment: Left 177 | PPIndentWidth: -1 178 | QualifierAlignment: Right 179 | ReferenceAlignment: Pointer 180 | ReflowComments: true 181 | RemoveBracesLLVM: false 182 | RemoveParentheses: Leave 183 | RemoveSemicolon: true 184 | RequiresClausePosition: WithPreceding 185 | RequiresExpressionIndentation: OuterScope 186 | SeparateDefinitionBlocks: Leave 187 | ShortNamespaceLines: 0 188 | SkipMacroDefinitionBody: false 189 | SortIncludes: CaseSensitive 190 | SortJavaStaticImport: Before 191 | SortUsingDeclarations: LexicographicNumeric 192 | SpaceAfterCStyleCast: false 193 | SpaceAfterLogicalNot: false 194 | SpaceAfterTemplateKeyword: false 195 | SpaceAroundPointerQualifiers: Default 196 | SpaceBeforeAssignmentOperators: true 197 | SpaceBeforeCaseColon: false 198 | SpaceBeforeCpp11BracedList: false 199 | SpaceBeforeCtorInitializerColon: true 200 | SpaceBeforeInheritanceColon: true 201 | SpaceBeforeJsonColon: false 202 | SpaceBeforeParens: ControlStatements 203 | SpaceBeforeParensOptions: 204 | AfterControlStatements: true 205 | AfterForeachMacros: true 206 | AfterFunctionDefinitionName: false 207 | AfterFunctionDeclarationName: false 208 | AfterIfMacros: true 209 | AfterOverloadedOperator: false 210 | AfterPlacementOperator: false 211 | AfterRequiresInClause: false 212 | AfterRequiresInExpression: false 213 | BeforeNonEmptyParentheses: false 214 | SpaceBeforeRangeBasedForLoopColon: true 215 | SpaceBeforeSquareBrackets: false 216 | SpaceInEmptyBlock: false 217 | SpacesBeforeTrailingComments: 1 218 | SpacesInAngles: Never 219 | SpacesInContainerLiterals: true 220 | SpacesInLineCommentPrefix: 221 | Minimum: 1 222 | Maximum: -1 223 | SpacesInParens: Never 224 | SpacesInParensOptions: 225 | InCStyleCasts: false 226 | InConditionalStatements: false 227 | InEmptyParentheses: false 228 | Other: false 229 | SpacesInSquareBrackets: false 230 | Standard: Latest 231 | StatementAttributeLikeMacros: 232 | - Q_EMIT 233 | StatementMacros: 234 | - Q_UNUSED 235 | - QT_REQUIRE_VERSION 236 | TabWidth: 2 237 | UseTab: Never 238 | VerilogBreakBetweenInstancePorts: true 239 | WhitespaceSensitiveMacros: 240 | - BOOST_PP_STRINGIZE 241 | - CF_SWIFT_NAME 242 | - NS_SWIFT_NAME 243 | - PP_STRINGIZE 244 | - STRINGIZE 245 | ... 246 | 247 | --------------------------------------------------------------------------------