├── .clang-format ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── differential_pressure_sensor ├── .idea │ └── dictionaries │ │ └── pavel.xml ├── CMakeLists.txt ├── README.md ├── docs │ ├── monitor-initial.png │ └── monitor.png └── src │ └── main.c ├── libcyphal_demo ├── .clang-format ├── .clang-tidy ├── CMakeLists.txt ├── CMakePresets.json ├── README.md └── src │ ├── CMakeLists.txt │ ├── any_transport_bag.hpp │ ├── application.cpp │ ├── application.hpp │ ├── exec_cmd_provider.hpp │ ├── file_downloader.hpp │ ├── main.cpp │ ├── no_cpp_heap.cpp │ ├── platform │ ├── block_memory_resource.hpp │ ├── bsd │ │ └── kqueue_single_threaded_executor.hpp │ ├── common_helpers.hpp │ ├── defines.hpp │ ├── linux │ │ ├── can │ │ │ └── can_media.hpp │ │ └── epoll_single_threaded_executor.hpp │ ├── o1_heap_memory_resource.hpp │ ├── posix │ │ ├── posix_executor_extension.hpp │ │ ├── posix_platform_error.hpp │ │ └── udp │ │ │ ├── udp_media.hpp │ │ │ └── udp_sockets.hpp │ ├── storage.hpp │ └── string.hpp │ ├── transport_bag_can.hpp │ └── transport_bag_udp.hpp ├── libudpard_demo ├── .clang-tidy ├── .idea │ └── dictionaries │ │ └── pavel.xml ├── CMakeLists.txt ├── README.md ├── docs │ ├── wireshark-pnp.png │ ├── yakut-monitor-data.png │ └── yakut-monitor-pnp.png └── src │ ├── crc64we.h │ ├── main.c │ ├── memory_block.h │ ├── register.c │ ├── register.h │ ├── storage.c │ └── storage.h ├── shared ├── register │ ├── register.c │ ├── register.cmake │ └── register.h ├── socketcan │ ├── socketcan.c │ ├── socketcan.cmake │ └── socketcan.h └── udp │ ├── udp.c │ ├── udp.cmake │ └── udp.h ├── submodules └── cavl │ └── cavl.h └── udral_servo ├── .idea └── dictionaries │ └── pavel.xml ├── CMakeLists.txt ├── README.md ├── docs ├── monitor-initial.png └── monitor.png └── src └── main.c /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: true 7 | AlignConsecutiveDeclarations: true 8 | AlignEscapedNewlines: Left 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: false 12 | AllowShortBlocksOnASingleLine: Never 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: Inline 15 | AllowShortIfStatementsOnASingleLine: Never 16 | AllowShortLoopsOnASingleLine: false 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: false 20 | AlwaysBreakTemplateDeclarations: Yes 21 | BinPackArguments: false 22 | BinPackParameters: false 23 | BraceWrapping: 24 | AfterCaseLabel: true 25 | AfterClass: true 26 | AfterControlStatement: true 27 | AfterEnum: true 28 | AfterFunction: true 29 | AfterNamespace: true 30 | AfterStruct: true 31 | AfterUnion: true 32 | BeforeCatch: true 33 | BeforeElse: true 34 | IndentBraces: false 35 | SplitEmptyFunction: false 36 | SplitEmptyRecord: false 37 | SplitEmptyNamespace: false 38 | AfterExternBlock: false # Keeps the contents un-indented. 39 | BreakBeforeBinaryOperators: None 40 | BreakBeforeBraces: Custom 41 | BreakBeforeTernaryOperators: true 42 | BreakConstructorInitializers: AfterColon 43 | # BreakInheritanceList: AfterColon 44 | BreakStringLiterals: true 45 | ColumnLimit: 120 46 | CommentPragmas: '^ (coverity|pragma:)' 47 | CompactNamespaces: false 48 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 49 | ConstructorInitializerIndentWidth: 4 50 | ContinuationIndentWidth: 4 51 | Cpp11BracedListStyle: true 52 | DerivePointerAlignment: false 53 | DisableFormat: false 54 | ExperimentalAutoDetectBinPacking: false 55 | FixNamespaceComments: true 56 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 57 | IncludeBlocks: Preserve 58 | IndentCaseLabels: false 59 | IndentPPDirectives: AfterHash 60 | IndentWidth: 4 61 | IndentWrappedFunctionNames: false 62 | KeepEmptyLinesAtTheStartOfBlocks: false 63 | MacroBlockBegin: '' 64 | MacroBlockEnd: '' 65 | MaxEmptyLinesToKeep: 1 66 | NamespaceIndentation: None 67 | PenaltyBreakAssignment: 2 68 | PenaltyBreakBeforeFirstCallParameter: 10000 # Raised intentionally; prefer breaking all 69 | PenaltyBreakComment: 300 70 | PenaltyBreakFirstLessLess: 120 71 | PenaltyBreakString: 1000 72 | PenaltyExcessCharacter: 1000000 73 | PenaltyReturnTypeOnItsOwnLine: 10000 # Raised intentionally because it hurts readability 74 | PointerAlignment: Left 75 | ReflowComments: true 76 | SortIncludes: Never 77 | SortUsingDeclarations: false 78 | SpaceAfterCStyleCast: true 79 | SpaceAfterTemplateKeyword: true 80 | SpaceBeforeAssignmentOperators: true 81 | SpaceBeforeCpp11BracedList: false 82 | SpaceBeforeInheritanceColon: true 83 | SpaceBeforeParens: ControlStatements 84 | SpaceBeforeCtorInitializerColon: true 85 | SpaceBeforeRangeBasedForLoopColon: true 86 | SpaceInEmptyParentheses: false 87 | SpacesBeforeTrailingComments: 2 88 | SpacesInAngles: false 89 | SpacesInCStyleCastParentheses: false 90 | SpacesInContainerLiterals: false 91 | SpacesInParentheses: false 92 | SpacesInSquareBrackets: false 93 | Standard: c++14 94 | TabWidth: 8 95 | UseTab: Never 96 | ... 97 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'issue/*' 8 | pull_request: 9 | branches: 10 | - main 11 | - 'issue/*' 12 | 13 | env: 14 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 15 | BUILD_TYPE: Release 16 | 17 | jobs: 18 | build_libcyphal_demo: 19 | name: Build LibCyphal demo 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | submodules: recursive 26 | 27 | - name: Install Ninja 28 | run: sudo apt-get install ninja-build 29 | 30 | - name: Configure CMake 31 | run: cd ${{github.workspace}}/libcyphal_demo && cmake --preset Demo-Linux 32 | 33 | - name: Build Debug 34 | run: cd ${{github.workspace}}/libcyphal_demo && cmake --build --preset Demo-Linux-Debug 35 | 36 | - name: Build Release 37 | run: cd ${{github.workspace}}/libcyphal_demo && cmake --build --preset Demo-Linux-Release 38 | 39 | build_libudpard_demo: 40 | name: Build LibUDPard demo 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | with: 46 | submodules: recursive 47 | 48 | - name: Configure CMake 49 | run: cmake -B ${{github.workspace}}/libudpard_demo/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${{github.workspace}}/libudpard_demo 50 | 51 | - name: Build 52 | run: cmake --build ${{github.workspace}}/libudpard_demo/build --config ${{env.BUILD_TYPE}} 53 | 54 | build_differential_pressure_sensor: 55 | name: Build Differential Pressure Sensor demo 56 | runs-on: ubuntu-latest 57 | 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | submodules: recursive 62 | 63 | - name: Configure CMake 64 | run: cmake -B ${{github.workspace}}/differential_pressure_sensor/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${{github.workspace}}/differential_pressure_sensor 65 | 66 | - name: Build 67 | run: cmake --build ${{github.workspace}}/differential_pressure_sensor/build --config ${{env.BUILD_TYPE}} 68 | 69 | 70 | build_udral_servo: 71 | name: Build UDRAL Servo demo 72 | runs-on: ubuntu-latest 73 | 74 | steps: 75 | - uses: actions/checkout@v4 76 | with: 77 | submodules: recursive 78 | 79 | - name: Configure CMake 80 | run: cmake -B ${{github.workspace}}/udral_servo/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${{github.workspace}}/udral_servo 81 | 82 | - name: Build 83 | run: cmake --build ${{github.workspace}}/udral_servo/build --config ${{env.BUILD_TYPE}} 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.d 2 | *.o 3 | *.ko 4 | *.obj 5 | *.elf 6 | *.ilk 7 | *.map 8 | *.exp 9 | *.gch 10 | *.pch 11 | *.lib 12 | *.a 13 | *.la 14 | *.lo 15 | *.dll 16 | *.so 17 | *.so.* 18 | *.dylib 19 | *.exe 20 | *.out 21 | *.app 22 | *.i*86 23 | *.x86_64 24 | *.hex 25 | *.dSYM/ 26 | *.su 27 | *.idb 28 | *.pdb 29 | *.mod* 30 | .tmp_versions/ 31 | modules.order 32 | Module.symvers 33 | Mkfile.old 34 | dkms.conf 35 | build/ 36 | cmake-build*/ 37 | 38 | # DSDL compilation outputs 39 | .compiled/ 40 | .transpiled/ 41 | 42 | # Node register files 43 | *.cfg 44 | 45 | # IDE and tools 46 | .gdbinit 47 | **/.idea/* 48 | !**/.idea/dictionaries 49 | !**/.idea/dictionaries/* 50 | .venv 51 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/libcanard"] 2 | path = submodules/libcanard 3 | url = https://github.com/UAVCAN/libcanard 4 | branch = v4 5 | [submodule "submodules/o1heap"] 6 | path = submodules/o1heap 7 | url = https://github.com/pavel-kirienko/o1heap 8 | [submodule "submodules/libudpard"] 9 | path = submodules/libudpard 10 | url = https://github.com/OpenCyphal/libudpard 11 | branch = v2 12 | [submodule "submodules/cetl"] 13 | path = submodules/cetl 14 | url = https://github.com/OpenCyphal/CETL.git 15 | [submodule "submodules/libcyphal"] 16 | path = submodules/libcyphal 17 | url = https://github.com/OpenCyphal-Garage/libcyphal.git 18 | [submodule "submodules/nunavut"] 19 | path = submodules/nunavut 20 | url = https://github.com/OpenCyphal/nunavut.git 21 | branch = 3.0.preview 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "baremetal", 4 | "cetl", 5 | "Cyphal", 6 | "libcyphal", 7 | "POSIX", 8 | "uavcan" 9 | ], 10 | "cmake.sourceDirectory": "/home/sergei/Develop/git/OpenCyphal-Garage/demos/libcyphal_demo" 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 OpenCyphal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo applications and references 2 | 3 | [![Forum](https://img.shields.io/discourse/users.svg?server=https%3A%2F%2Fforum.opencyphal.org&color=1700b3)](https://forum.opencyphal.org) 4 | 5 | A weakly organized collection of usage demos and examples that can be used to bootstrap product development. 6 | 7 | 8 | ## Background 9 | 10 | Adopting Cyphal may seem like a paradigm shift for an engineer experienced with prior-art technologies 11 | due to its focus on service-orientation and zero-cost abstraction. 12 | In order to make sense of the materials presented here, 13 | **you should first read [the Cyphal Guide](https://opencyphal.org/guide)**. 14 | For a more hands-on experience, consider completing the 15 | [PyCyphal tutorial](https://pycyphal.readthedocs.io/en/stable/pages/demo.html). 16 | 17 | 18 | ## How to use this repository 19 | 20 | There is a separate directory per demo. 21 | Demos may depend on the components published by the OpenCyphal team, such as 22 | [Libcanard](https://github.com/OpenCyphal/libcanard) or the 23 | [public regulated DSDL definitions](https://github.com/OpenCyphal/public_regulated_data_types/). 24 | These are collected under `submodules/`. 25 | You will need to add them to your application separately in whatever way suits your workflow best --- 26 | as a Git submodule, by copy-pasting the sources, using CMake's `ExternalProject_Add()`, etc. 27 | -------------------------------------------------------------------------------- /differential_pressure_sensor/.idea/dictionaries/pavel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | afdx 5 | allocatee 6 | antipattern 7 | appveyor 8 | ardu 9 | argparse 10 | asctime 11 | asyncio 12 | atexit 13 | aton 14 | autoconfiguration 15 | autodoc 16 | baremetal 17 | baudrate 18 | bgcolor 19 | blin 20 | bools 21 | bufferless 22 | byref 23 | bysource 24 | candump 25 | canfd 26 | canid 27 | caplog 28 | capturable 29 | ccitt 30 | cmsg 31 | coloredlogs 32 | computron 33 | comspec 34 | conftest 35 | constrainedness 36 | constructible 37 | contnode 38 | creationflags 39 | ctrunc 40 | cyber 41 | cyphal 42 | datagrams 43 | deadbeef 44 | deallocated 45 | debian 46 | decaxta 47 | deduplicator 48 | deduplicators 49 | demultiplexer 50 | demultiplexing 51 | demux 52 | dereplicated 53 | deserializing 54 | dhcp 55 | diehard 56 | disbalance 57 | dlen 58 | docname 59 | doctree 60 | doesn 61 | dronecode 62 | dscp 63 | dsdl 64 | dsonar 65 | dtype 66 | elif 67 | emptor 68 | endfor 69 | endmacro 70 | ethertype 71 | facto 72 | ffee 73 | ffff 74 | fgsfds 75 | findalldevs 76 | fontname 77 | frombuffer 78 | fyodor 79 | geez 80 | gendsdl 81 | genericity 82 | getenv 83 | getpeername 84 | getsockopt 85 | gibibyte 86 | gibibytes 87 | graphviz 88 | hacky 89 | hardbass 90 | hdlc 91 | hitl 92 | hostmask 93 | hwgrep 94 | hwmaj 95 | hwmin 96 | iana 97 | icmp 98 | iface 99 | ifaces 100 | ifidx 101 | inaddr 102 | inet 103 | intersphinx 104 | intravehicular 105 | ipproto 106 | iscsi 107 | isfinite 108 | kibibyte 109 | kirienko 110 | koopman 111 | levelname 112 | libcanard 113 | libpcap 114 | libuavcan 115 | linkcode 116 | lnid 117 | lsmod 118 | mcfloatface 119 | mebibytes 120 | memcpy 121 | memoryview 122 | mismaintenance 123 | modifyitems 124 | modprobe 125 | mult 126 | multiframe 127 | multithreaded 128 | mypy 129 | mypypath 130 | ncat 131 | ndarray 132 | ndim 133 | ndis 134 | netcat 135 | netfilter 136 | netstat 137 | nfrag 138 | nihil 139 | nmap 140 | nnvg 141 | noqa 142 | norecursedirs 143 | nosignatures 144 | npcap 145 | ntdll 146 | octothorp 147 | onboard 148 | opencyphal 149 | packbits 150 | pathlib 151 | pcap 152 | pcapy 153 | perfcounters 154 | pfft 155 | pitot 156 | pizdec 157 | pizdets 158 | pkgutil 159 | popen 160 | powershell 161 | prio 162 | prog 163 | protip 164 | pydev 165 | pydsdl 166 | pydsdlgen 167 | pygments 168 | pylint 169 | pyserial 170 | pytest 171 | pythonasynciodebug 172 | pythoncan 173 | pythonpath 174 | pythonunbuffered 175 | pyuavcan 176 | pyyaml 177 | quantizer 178 | qube 179 | qwertyui 180 | qwertyuiop 181 | raii 182 | rankdir 183 | rawsource 184 | readlines 185 | readthedocs 186 | reasm 187 | reasms 188 | reassembler 189 | reassemblers 190 | rechunk 191 | recvfrom 192 | recvmsg 193 | refdoc 194 | refid 195 | refragment 196 | refragmented 197 | reftarget 198 | reftype 199 | reftypes 200 | relbar 201 | relbars 202 | representer 203 | representers 204 | repurposeability 205 | reuseport 206 | roundtrip 207 | rtfd 208 | rtps 209 | ruamel 210 | runas 211 | sapog 212 | searchbox 213 | sendable 214 | sendmsg 215 | sert 216 | serv 217 | setcap 218 | setpoint 219 | setsockopt 220 | sheremet 221 | signedness 222 | sitecustomize 223 | sitl 224 | slcan 225 | sockaddr 226 | socketcan 227 | socketcanfd 228 | sphinxarg 229 | sssss 230 | ssssssss 231 | stdint 232 | strictification 233 | subcommand 234 | subcommands 235 | sublayers 236 | submoduling 237 | subnets 238 | subparser 239 | suka 240 | supernum 241 | supremum 242 | swmaj 243 | swmin 244 | synth 245 | systeminfo 246 | telega 247 | tempfile 248 | templatedir 249 | testpaths 250 | thumby 251 | timestamping 252 | tobytes 253 | tocfile 254 | toctree 255 | todos 256 | tradeoff 257 | typecheck 258 | uart 259 | uavcan 260 | uber 261 | udpros 262 | udral 263 | ulong 264 | uname 265 | unconfigured 266 | undisable 267 | undoc 268 | unhashable 269 | unicast 270 | unseparate 271 | unstropped 272 | upvote 273 | usec 274 | usercustomize 275 | vcan 276 | versioning 277 | veyor 278 | virtualization 279 | voldemort 280 | vpaun 281 | vssc 282 | weakref 283 | winpcap 284 | wireshark 285 | worl 286 | wpcap 287 | xbee 288 | xfer 289 | zubax 290 | 291 | 292 | -------------------------------------------------------------------------------- /differential_pressure_sensor/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This software is distributed under the terms of the MIT License. 2 | # Copyright (C) 2021 OpenCyphal 3 | # Author: Pavel Kirienko 4 | 5 | cmake_minimum_required(VERSION 3.17) 6 | project(differential_pressure_sensor C) 7 | 8 | set(submodules "${CMAKE_CURRENT_SOURCE_DIR}/../submodules") 9 | set(CMAKE_PREFIX_PATH "${submodules}/nunavut") 10 | 11 | set(CMAKE_C_STANDARD 11) 12 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Werror -pedantic -fstrict-aliasing") 13 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wdouble-promotion -Wswitch-enum -Wfloat-equal -Wundef") 14 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wconversion -Wtype-limits") 15 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wsign-conversion -Wcast-align -Wmissing-declarations") 16 | 17 | # Forward the revision information to the compiler so that we could expose it at runtime. This is entirely optional. 18 | execute_process( 19 | COMMAND git rev-parse --short=16 HEAD 20 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 21 | OUTPUT_VARIABLE vcs_revision_id 22 | OUTPUT_STRIP_TRAILING_WHITESPACE 23 | ) 24 | message(STATUS "vcs_revision_id: ${vcs_revision_id}") 25 | add_definitions( 26 | -DVERSION_MAJOR=1 27 | -DVERSION_MINOR=0 28 | -DVCS_REVISION_ID=0x${vcs_revision_id}ULL 29 | -DNODE_NAME="org.opencyphal.demos.differential_pressure" 30 | ) 31 | 32 | ## Transpile DSDL into C using Nunavut. This uses this repo's built-in submodules to setup Nunavut. See 33 | # CMAKE_PREFIX_PATH above for how this is resolved to the local submodules. 34 | find_package(Nunavut 3.0 REQUIRED) 35 | 36 | set(LOCAL_PUBLIC_TYPES 37 | uavcan/node/430.GetInfo.1.0.dsdl 38 | uavcan/node/435.ExecuteCommand.1.1.dsdl 39 | uavcan/node/7509.Heartbeat.1.0.dsdl 40 | uavcan/node/port/7510.List.0.1.dsdl 41 | uavcan/pnp/8165.NodeIDAllocationData.2.0.dsdl 42 | uavcan/register/384.Access.1.0.dsdl 43 | uavcan/register/385.List.1.0.dsdl 44 | uavcan/si/unit/pressure/Scalar.1.0.dsdl 45 | uavcan/si/unit/temperature/Scalar.1.0.dsdl 46 | ) 47 | 48 | add_cyphal_library( 49 | NAME dsdl_uavcan 50 | EXACT_NAME 51 | LANGUAGE c 52 | LANGUAGE_STANDARD c${CMAKE_C_STANDARD} 53 | DSDL_FILES ${LOCAL_PUBLIC_TYPES} 54 | SERIALIZATION_ASSERT assert 55 | EXPORT_MANIFEST 56 | OUT_LIBRARY_TARGET LOCAL_TYPES_C_LIBRARY 57 | ) 58 | 59 | # Build libcanard. 60 | add_library(canard STATIC ${submodules}/libcanard/libcanard/canard.c) 61 | include_directories(SYSTEM ${submodules}/libcanard/libcanard) 62 | 63 | # Build o1heap -- a hard real-time deterministic memory allocator for embedded systems. 64 | add_library(o1heap STATIC ${submodules}/o1heap/o1heap/o1heap.c) 65 | include_directories(SYSTEM ${submodules}/o1heap/o1heap/) 66 | 67 | include(${CMAKE_CURRENT_SOURCE_DIR}/../shared/register/register.cmake) 68 | target_link_libraries(shared_register 69 | PRIVATE ${LOCAL_TYPES_C_LIBRARY} 70 | ) 71 | 72 | include(${CMAKE_CURRENT_SOURCE_DIR}/../shared/socketcan/socketcan.cmake) 73 | 74 | # Build the application. 75 | add_executable(differential_pressure_sensor 76 | src/main.c 77 | ) 78 | target_link_libraries(differential_pressure_sensor 79 | ${LOCAL_TYPES_C_LIBRARY} 80 | canard 81 | o1heap 82 | shared_register 83 | shared_socketcan 84 | ) 85 | -------------------------------------------------------------------------------- /differential_pressure_sensor/README.md: -------------------------------------------------------------------------------- 1 | # Differential pressure sensor demo 2 | 3 | ## Purpose 4 | 5 | This demo implements a simple differential pressure & static air temperature sensor node 6 | in a highly portable C application that can be trivially adapted to run in a baremetal environment. 7 | Unless ported, the demo is intended for evaluation on GNU/Linux. 8 | 9 | This demo supports only Cyphal/CAN at the moment, but it can be extended to support Cyphal/UDP or Cyphal/serial. 10 | 11 | 12 | ## Preparation 13 | 14 | You will need GNU/Linux, CMake, a C11 compiler, [Yakut](https://github.com/OpenCyphal/yakut), 15 | and [SocketCAN utils](https://github.com/linux-can/can-utils). 16 | 17 | Build the demo as follows: 18 | 19 | ```bash 20 | git clone --recursive https://github.com/OpenCyphal/demos 21 | cd demos/differential_pressure_sensor 22 | mkdir build && cd build 23 | cmake .. && make 24 | ``` 25 | 26 | 27 | ## Running 28 | 29 | Set up a virtual CAN bus `vcan0`: 30 | 31 | ```bash 32 | modprobe can 33 | modprobe can_raw 34 | modprobe vcan 35 | ip link add dev vcan0 type vcan 36 | ip link set vcan0 mtu 72 # Enable CAN FD by configuring the MTU of 64+8 37 | ip link set up vcan0 38 | ``` 39 | 40 | Launch the node 41 | (it is built to emulate an embedded system so it does not accept any arguments or environment variables): 42 | 43 | ```bash 44 | ./differential_pressure_sensor 45 | ``` 46 | 47 | It may print a few informational messages and then go silent. 48 | 49 | Fire up the CAN dump utility from SocketCAN utils and see what's happening on the bus. 50 | You should see the PnP node-ID allocation requests being sent by our node irregularly: 51 | 52 | ```bash 53 | $ candump -decaxta vcan0 54 | (1616445708.288978) vcan0 TX B - 197FE510 [20] FF FF C6 69 73 51 FF 4A EC 29 CD BA AB F2 FB E3 46 7C 00 E9 55 | (1616445711.289044) vcan0 TX B - 197FE510 [20] FF FF C6 69 73 51 FF 4A EC 29 CD BA AB F2 FB E3 46 7C 00 EA 56 | # and so on... 57 | ``` 58 | 59 | It will keep doing this forever until it got an allocation response from the node-ID allocator. 60 | 61 | Next, we launch a PnP node-ID allocator available in Yakut (PX4 also implements one): 62 | 63 | ```bash 64 | export UAVCAN__CAN__IFACE="socketcan:vcan0" 65 | export UAVCAN__NODE__ID=127 # This node-ID is for Yakut. 66 | y mon --plug-and-play ~/allocation_table.db 67 | ``` 68 | 69 | This command will run the monitor together with the allocator. 70 | You will see our node get itself a node-ID allocated, 71 | then roughly the following picture should appear on the monitor: 72 | 73 | yakut monitor 74 | 75 | That means that our node is running, but it is unable to publish measurements because the respective subjects 76 | remain unconfigured. 77 | So let's configure them (do not stop the monitor though, otherwise you won't know what's happening on the bus), 78 | assuming that the node got allocated the node-ID of 125. 79 | First, it helps to know what registers are available at all: 80 | 81 | ```bash 82 | $ export UAVCAN__CAN__IFACE="socketcan:vcan0" 83 | $ export UAVCAN__NODE__ID=126 # This node-ID is for Yakut. 84 | $ y rl 125 85 | [reg.udral.service.pitot, uavcan.can.mtu, uavcan.node.description, uavcan.node.id, uavcan.node.unique_id, uavcan.pub.airspeed.differential_pressure.id, uavcan.pub.airspeed.differential_pressure.type, uavcan.pub.airspeed.static_air_temperature.id, uavcan.pub.airspeed.static_air_temperature.type, udral.pnp.cookie] 86 | $ y rl 125, | y rb # You can also read all registers like this 87 | # (output not shown) 88 | ``` 89 | 90 | Configure the subject-IDs: 91 | 92 | ```bash 93 | y r 125 uavcan.pub.airspeed.differential_pressure.id 100 94 | y r 125 uavcan.pub.airspeed.static_air_temperature.id 101 95 | ``` 96 | 97 | The node is configured now, but we need to restart it before the configuration parameter changes take effect: 98 | 99 | ```bash 100 | y cmd 125 restart -e 101 | ``` 102 | 103 | You should see candump start printing a lot more frames because the demo is now publishing the sensor data. 104 | The monitor will also show the subjects that we just configured. 105 | 106 | yakut monitor 107 | 108 | You can subscribe to the published differential pressure using Yakut as follows: 109 | 110 | ```bash 111 | y sub 100:uavcan.si.unit.pressure.scalar 112 | ``` 113 | 114 | You can erase the configuration and go back to factory defaults as follows: 115 | 116 | ```bash 117 | y cmd 125 factory_reset 118 | ``` 119 | 120 | 121 | ## Porting 122 | 123 | Just read the code. 124 | 125 | The files `socketcan.[ch]` were taken from . 126 | You may (or may not) find something relevant for your target platform there, too. 127 | -------------------------------------------------------------------------------- /differential_pressure_sensor/docs/monitor-initial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCyphal-Garage/demos/87741d8242bcb27b39e22115559a4b91e92ffe06/differential_pressure_sensor/docs/monitor-initial.png -------------------------------------------------------------------------------- /differential_pressure_sensor/docs/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCyphal-Garage/demos/87741d8242bcb27b39e22115559a4b91e92ffe06/differential_pressure_sensor/docs/monitor.png -------------------------------------------------------------------------------- /libcyphal_demo/.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: true 7 | AlignConsecutiveDeclarations: true 8 | AlignEscapedNewlines: Left 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: false 12 | AllowShortBlocksOnASingleLine: Never 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: Empty 15 | AllowShortIfStatementsOnASingleLine: false 16 | AllowShortLoopsOnASingleLine: false 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: false 20 | AlwaysBreakTemplateDeclarations: Yes 21 | BinPackArguments: false 22 | BinPackParameters: false 23 | BreakBeforeBinaryOperators: None 24 | BreakBeforeBraces: Custom 25 | BraceWrapping: 26 | SplitEmptyRecord: false 27 | AfterEnum: true 28 | AfterStruct: true 29 | AfterClass: true 30 | AfterControlStatement: true 31 | AfterFunction: true 32 | AfterUnion: true 33 | AfterNamespace: true 34 | AfterExternBlock: true 35 | BeforeElse: true 36 | 37 | BreakBeforeTernaryOperators: true 38 | BreakConstructorInitializers: BeforeComma 39 | BreakStringLiterals: true 40 | ColumnLimit: 120 41 | CommentPragmas: '^ (coverity|NOSONAR|pragma:)' 42 | CompactNamespaces: false 43 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 44 | ConstructorInitializerIndentWidth: 4 45 | ContinuationIndentWidth: 4 46 | Cpp11BracedListStyle: true 47 | DerivePointerAlignment: false 48 | DisableFormat: false 49 | FixNamespaceComments: true 50 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 51 | IncludeBlocks: Preserve 52 | IndentCaseLabels: false 53 | IndentPPDirectives: AfterHash 54 | IndentWidth: 4 55 | IndentWrappedFunctionNames: false 56 | KeepEmptyLinesAtTheStartOfBlocks: false 57 | MacroBlockBegin: '' 58 | MacroBlockEnd: '' 59 | MaxEmptyLinesToKeep: 1 60 | NamespaceIndentation: None 61 | PenaltyBreakAssignment: 2 62 | PenaltyBreakBeforeFirstCallParameter: 10000 # Raised intentionally; prefer breaking all 63 | PenaltyBreakComment: 300 64 | PenaltyBreakFirstLessLess: 120 65 | PenaltyBreakString: 1000 66 | PenaltyExcessCharacter: 1000000 67 | PenaltyReturnTypeOnItsOwnLine: 10000 # Raised intentionally because it hurts readability 68 | PointerAlignment: Left 69 | ReflowComments: true 70 | SortIncludes: Never 71 | SortUsingDeclarations: false 72 | SpaceAfterCStyleCast: true 73 | SpaceAfterTemplateKeyword: true 74 | SpaceBeforeAssignmentOperators: true 75 | SpaceBeforeParens: ControlStatements 76 | SpaceInEmptyParentheses: false 77 | SpacesBeforeTrailingComments: 2 78 | SpacesInAngles: false 79 | SpacesInCStyleCastParentheses: false 80 | SpacesInContainerLiterals: false 81 | SpacesInParentheses: false 82 | SpacesInSquareBrackets: false 83 | Standard: c++14 84 | TabWidth: 8 85 | UseTab: Never 86 | ... 87 | -------------------------------------------------------------------------------- /libcyphal_demo/.clang-tidy: -------------------------------------------------------------------------------- 1 | Checks: >- 2 | boost-*, 3 | bugprone-*, 4 | cert-*, 5 | clang-analyzer-*, 6 | cppcoreguidelines-*, 7 | google-*, 8 | hicpp-*, 9 | llvm-*, 10 | misc-*, 11 | modernize-*, 12 | performance-*, 13 | portability-*, 14 | readability-*, 15 | -clang-analyzer-core.uninitialized.Assign, 16 | -cppcoreguidelines-avoid-const-or-ref-data-members, 17 | -cppcoreguidelines-use-default-member-init, 18 | -google-readability-avoid-underscore-in-googletest-name, 19 | -google-readability-todo, 20 | -llvm-header-guard, 21 | -modernize-concat-nested-namespaces, 22 | -modernize-type-traits, 23 | -modernize-use-constraints, 24 | -modernize-use-default-member-init, 25 | -modernize-use-nodiscard, 26 | -readability-avoid-const-params-in-decls, 27 | -readability-identifier-length, 28 | -*-use-trailing-return-type, 29 | -*-named-parameter, 30 | CheckOptions: 31 | - key: readability-function-cognitive-complexity.Threshold 32 | value: '90' 33 | - key: readability-magic-numbers.IgnoredIntegerValues 34 | value: '1;2;3;4;5;8;10;16;20;32;60;64;100;128;256;500;512;1000' 35 | WarningsAsErrors: '*' 36 | FormatStyle: file 37 | -------------------------------------------------------------------------------- /libcyphal_demo/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This software is distributed under the terms of the MIT License. 2 | # Copyright (C) OpenCyphal Development Team 3 | # Copyright Amazon.com Inc. or its affiliates. 4 | # SPDX-License-Identifier: MIT 5 | # Author: Sergei Shirokov 6 | 7 | cmake_minimum_required(VERSION 3.25) 8 | 9 | project(libcyphal_demo 10 | LANGUAGES CXX C 11 | HOMEPAGE_URL https://github.com/OpenCyphal-Garage/libcyphal) 12 | 13 | set(CMAKE_CXX_STANDARD "14" CACHE STRING "C++ standard to use when compiling.") 14 | set(DISABLE_CPP_EXCEPTIONS ON CACHE STRING "Disable C++ exceptions.") 15 | 16 | option(CETL_ENABLE_DEBUG_ASSERT "Enable or disable runtime CETL asserts." ON) 17 | 18 | set(CXX_FLAG_SET "") 19 | if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 20 | if (DISABLE_CPP_EXCEPTIONS) 21 | message(STATUS "DISABLE_CPP_EXCEPTIONS is true. Adding -fno-exceptions to compiler flags.") 22 | list(APPEND CXX_FLAG_SET "-fno-exceptions") 23 | endif() 24 | endif() 25 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 26 | # Disable PSABI warnings in GCC (on RPi). 27 | list(APPEND CXX_FLAG_SET "-Wno-psabi") 28 | endif() 29 | add_compile_options("$<$:${CXX_FLAG_SET}>") 30 | 31 | if (CETL_ENABLE_DEBUG_ASSERT) 32 | add_compile_definitions("CETL_ENABLE_DEBUG_ASSERT=1") 33 | endif() 34 | 35 | # Set the output binary directory 36 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 37 | 38 | set(submodules_dir "${CMAKE_SOURCE_DIR}/../submodules") 39 | 40 | # Set up static analysis. 41 | set(STATIC_ANALYSIS ON CACHE BOOL "enable static analysis") 42 | if (STATIC_ANALYSIS) 43 | # clang-tidy (separate config files per directory) 44 | find_program(clang_tidy NAMES clang-tidy) 45 | if (NOT clang_tidy) 46 | message(WARNING "Could not locate clang-tidy") 47 | endif () 48 | message(STATUS "Using clang-tidy: ${clang_tidy}") 49 | endif () 50 | 51 | # Pull in Nunavut's cmake integration 52 | find_package("Nunavut" 3.0 REQUIRED) 53 | 54 | # libcyphal requires PMR support for Nunavut generated code. 55 | if (${CMAKE_CXX_STANDARD} STREQUAL "14") 56 | set(CYPHAL_LANGUAGE_STANDARD "cetl++14-17") 57 | else () 58 | set(CYPHAL_LANGUAGE_STANDARD "c++${CMAKE_CXX_STANDARD}-pmr") 59 | endif () 60 | 61 | # Forward the revision information to the compiler so that we could expose it at runtime. This is entirely optional. 62 | execute_process( 63 | COMMAND git rev-parse --short=16 HEAD 64 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 65 | OUTPUT_VARIABLE vcs_revision_id 66 | OUTPUT_STRIP_TRAILING_WHITESPACE 67 | ) 68 | message(STATUS "vcs_revision_id: ${vcs_revision_id}") 69 | add_definitions( 70 | -DVERSION_MAJOR=1 71 | -DVERSION_MINOR=0 72 | -DVCS_REVISION_ID=0x${vcs_revision_id}ULL 73 | -DNODE_NAME="org.opencyphal.demos.libcyphal" 74 | ) 75 | if (DEFINED PLATFORM_OS_TYPE) 76 | if (${PLATFORM_OS_TYPE} STREQUAL "bsd") 77 | add_definitions(-DPLATFORM_OS_TYPE_BSD) 78 | elseif (${PLATFORM_OS_TYPE} STREQUAL "linux") 79 | add_definitions(-DPLATFORM_OS_TYPE_LINUX) 80 | endif () 81 | endif () 82 | 83 | add_definitions(-DNUNAVUT_ASSERT=assert) 84 | 85 | # Define the LibUDPard static library build target. 86 | add_library(udpard STATIC ${submodules_dir}/libudpard/libudpard/udpard.c) 87 | target_include_directories(udpard INTERFACE SYSTEM ${submodules_dir}/libudpard/libudpard) 88 | include(${CMAKE_SOURCE_DIR}/../shared/udp/udp.cmake) 89 | 90 | if (${PLATFORM_OS_TYPE} STREQUAL "linux") 91 | # Define the LibCANard static library build target. 92 | add_library(canard STATIC ${submodules_dir}/libcanard/libcanard/canard.c) 93 | target_include_directories(canard INTERFACE SYSTEM ${submodules_dir}/libcanard/libcanard) 94 | include(${CMAKE_SOURCE_DIR}/../shared/socketcan/socketcan.cmake) 95 | endif () 96 | 97 | # Build o1heap -- a hard real-time deterministic memory allocator for embedded systems. 98 | add_library(o1heap STATIC ${submodules_dir}/o1heap/o1heap/o1heap.c) 99 | target_include_directories(o1heap INTERFACE SYSTEM ${submodules_dir}/o1heap/o1heap) 100 | 101 | add_subdirectory(src) 102 | -------------------------------------------------------------------------------- /libcyphal_demo/CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 6, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 25, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "config-common", 11 | "hidden": true, 12 | "description": "Common configuration", 13 | "generator": "Ninja Multi-Config", 14 | "binaryDir": "${sourceDir}/build", 15 | "warnings": { 16 | "deprecated": true, 17 | "uninitialized": true 18 | }, 19 | "cacheVariables": { 20 | "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", 21 | "CMAKE_CONFIGURATION_TYPES": "Release;Debug", 22 | "CMAKE_CROSS_CONFIGS": "all", 23 | "CMAKE_DEFAULT_BUILD_TYPE": "Release", 24 | "CMAKE_DEFAULT_CONFIGS": "Release", 25 | "CMAKE_PREFIX_PATH": "${sourceDir}/../submodules/nunavut", 26 | "CMAKE_CXX_FLAGS": "-DCETL_ENABLE_DEBUG_ASSERT=1" 27 | } 28 | }, 29 | { 30 | "name": "config-linux", 31 | "hidden": true, 32 | "cacheVariables": { 33 | "PLATFORM_OS_TYPE": "linux" 34 | } 35 | }, 36 | { 37 | "name": "config-bsd", 38 | "hidden": true, 39 | "cacheVariables": { 40 | "PLATFORM_OS_TYPE": "bsd" 41 | } 42 | }, 43 | { 44 | "name": "Demo-Linux", 45 | "displayName": "Linux Demo", 46 | "description": "Configures Demo for Linux.", 47 | "inherits": [ 48 | "config-common", 49 | "config-linux" 50 | ] 51 | }, 52 | { 53 | "name": "Demo-Linux-Coverage", 54 | "displayName": "Linux Demo (Coverage)", 55 | "description": "Configures Demo for Linux with coverage.", 56 | "inherits": [ 57 | "config-common", 58 | "config-linux" 59 | ], 60 | "binaryDir": "${sourceDir}/cmake-build-coverage", 61 | "cacheVariables": { 62 | "CMAKE_C_FLAGS": "--coverage", 63 | "CMAKE_CXX_FLAGS": "--coverage", 64 | "NO_STATIC_ANALYSIS": "ON" 65 | } 66 | }, 67 | { 68 | "name": "Demo-BSD", 69 | "displayName": "BSD Demo", 70 | "description": "Configures Demo for BSD", 71 | "inherits": [ 72 | "config-common", 73 | "config-bsd" 74 | ], 75 | "cacheVariables": { 76 | "CMAKE_C_COMPILER": "clang", 77 | "CMAKE_CXX_COMPILER": "clang++" 78 | } 79 | } 80 | ], 81 | "buildPresets": [ 82 | { 83 | "name": "Demo-Linux-Debug", 84 | "displayName": "Linux Demo (Debug)", 85 | "description": "Builds Demo for Linux", 86 | "configurePreset": "Demo-Linux", 87 | "configuration": "Debug" 88 | }, 89 | { 90 | "name": "Demo-Linux-Debug-Coverage", 91 | "displayName": "Linux Demo (Debug, Coverage)", 92 | "description": "Builds Demo for Linux with coverage", 93 | "configurePreset": "Demo-Linux-Coverage", 94 | "configuration": "Debug" 95 | }, 96 | { 97 | "name": "Demo-Linux-Release", 98 | "displayName": "Linux Demo (Release)", 99 | "description": "Builds Demo for Linux", 100 | "configurePreset": "Demo-Linux", 101 | "configuration": "Release" 102 | }, 103 | { 104 | "name": "Demo-BSD-Debug", 105 | "displayName": "BSD Demo (Debug)", 106 | "description": "Builds Demo for BSD", 107 | "configurePreset": "Demo-BSD", 108 | "configuration": "Debug" 109 | }, 110 | { 111 | "name": "Demo-BSD-Release", 112 | "displayName": "BSD Demo (Release)", 113 | "description": "Builds Demo for BSD", 114 | "configurePreset": "Demo-BSD", 115 | "configuration": "Release" 116 | } 117 | ], 118 | "testPresets": [ 119 | { 120 | "name": "Demo-Debug", 121 | "displayName": "Test Demo (Debug)", 122 | "description": "Tests Demo", 123 | "configurePreset": "Demo-Linux", 124 | "configuration": "Debug" 125 | }, 126 | { 127 | "name": "Demo-Release", 128 | "displayName": "Test Demo (Release)", 129 | "description": "Tests Demo", 130 | "configurePreset": "Demo-Linux", 131 | "configuration": "Release" 132 | } 133 | ] 134 | } 135 | -------------------------------------------------------------------------------- /libcyphal_demo/README.md: -------------------------------------------------------------------------------- 1 | # LibCyphal demo application 2 | 3 | This demo application is a usage demonstrator for [LibCyphal](https://github.com/OpenCyphal-Garage/libcyphal) --- 4 | a compact multi-transport Cyphal implementation for high-integrity systems written in C++14 and above. 5 | It implements a simple Cyphal node that showcases the following features: 6 | 7 | - Fixed port-ID and non-fixed port-ID publishers. 8 | - Fixed port-ID and non-fixed port-ID subscribers. 9 | - Fixed port-ID RPC server. 10 | - Plug-and-play node-ID allocation unless it is configured statically. 11 | - Fast Cyphal Register API and non-volatile storage for the persistent registers. 12 | - Support for redundant network interfaces. 13 | 14 | This document will walk you through the process of building, running, and evaluating the demo 15 | on a GNU/Linux-based OS. 16 | It can be easily ported to another platform, such as a baremetal MCU, 17 | by replacing the POSIX socket API and stdio with suitable alternatives; 18 | for details, please consult with: 19 | - `platform/posix/udp/*` files regarding UDP transport 20 | - `platform/linux/can/*` files regarding CAN transport 21 | - `platform/linux/epoll_single_threaded_executor.hpp` file regarding the executor using epoll (Debian) 22 | - `platform/linux/kqueue_single_threaded_executor.hpp` file regarding the executor using kqueue (BSD/Darwin) 23 | - `platform/storage.hpp` file regarding the non-volatile storage 24 | - `platform/o1_heap_memory_resource.hpp` file regarding the memory resource 25 | 26 | ## Preparation 27 | 28 | You will need GNU/Linux, CMake, a C++14 compiler. 29 | Install: 30 | - [Yakut](https://github.com/OpenCyphal/yakut) CLI tool, 31 | - For CAN transport [SocketCAN utils](https://github.com/linux-can/can-utils) 32 | - For UDP transport Wireshark with the [Cyphal plugins](https://github.com/OpenCyphal/wireshark_plugins) 33 | 34 | Build the demo: 35 | 36 | ```shell 37 | git clone --recursive https://github.com/OpenCyphal/demos 38 | cd demos/libcyphal_demo 39 | ``` 40 | Then one of the two presets depending on your system: 41 | 42 | - `Demo-Linux` - Debian-based Linux distros like Ubuntu. 43 | - `Demo-BSD` – Mac OS 44 | 45 | ``` 46 | cmake --preset Demo-Linux 47 | cmake --build --preset Demo-Linux-Debug 48 | ``` 49 | -------------------------------------------------------------------------------- /libcyphal_demo/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This software is distributed under the terms of the MIT License. 2 | # Copyright (C) OpenCyphal Development Team 3 | # Copyright Amazon.com Inc. or its affiliates. 4 | # SPDX-License-Identifier: MIT 5 | # Author: Sergei Shirokov 6 | 7 | cmake_minimum_required(VERSION 3.25) 8 | 9 | # Define type generation and header library all in one go. 10 | # 11 | set(dsdl_types_in_demo # List all the DSDL types used in the engine 12 | uavcan/file/405.GetInfo.0.2.dsdl 13 | uavcan/file/408.Read.1.1.dsdl 14 | uavcan/node/430.GetInfo.1.0.dsdl 15 | uavcan/node/435.ExecuteCommand.1.3.dsdl 16 | uavcan/node/7509.Heartbeat.1.0.dsdl 17 | uavcan/register/384.Access.1.0.dsdl 18 | uavcan/register/385.List.1.0.dsdl 19 | ) 20 | add_cyphal_library( 21 | NAME demo 22 | DSDL_FILES ${dsdl_types_in_demo} 23 | ALLOW_EXPERIMENTAL_LANGUAGES 24 | LANGUAGE cpp 25 | LANGUAGE_STANDARD ${CYPHAL_LANGUAGE_STANDARD} 26 | OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/dsdl_transpiled 27 | OUT_LIBRARY_TARGET demo_transpiled 28 | ) 29 | 30 | # Define the demo application build target and link it with the library. 31 | add_executable(demo 32 | ${CMAKE_SOURCE_DIR}/src/application.cpp 33 | ${CMAKE_SOURCE_DIR}/src/main.cpp 34 | ${CMAKE_SOURCE_DIR}/src/no_cpp_heap.cpp 35 | ) 36 | 37 | target_link_libraries(demo 38 | PRIVATE o1heap udpard shared_udp 39 | PRIVATE ${demo_transpiled} 40 | ) 41 | if (${PLATFORM_OS_TYPE} STREQUAL "linux") 42 | target_link_libraries(demo 43 | PRIVATE canard shared_socketcan 44 | ) 45 | endif () 46 | 47 | target_include_directories(demo PRIVATE ${CMAKE_SOURCE_DIR}/src) 48 | target_include_directories(demo PRIVATE ${submodules_dir}/cetl/include) 49 | target_include_directories(demo PRIVATE ${submodules_dir}/libcyphal/include) 50 | 51 | if (STATIC_ANALYSIS) 52 | set_target_properties(demo PROPERTIES C_CLANG_TIDY "${clang_tidy}") 53 | endif () 54 | -------------------------------------------------------------------------------- /libcyphal_demo/src/any_transport_bag.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef ANY_TRANSPORT_BAG_HPP_INCLUDED 8 | #define ANY_TRANSPORT_BAG_HPP_INCLUDED 9 | 10 | #include 11 | #include 12 | 13 | /// Represents storage of some (UDP, CAN) libcyphal transport and its media. 14 | /// 15 | class AnyTransportBag 16 | { 17 | public: 18 | using Ptr = libcyphal::UniquePtr; 19 | using Transport = libcyphal::transport::ITransport; 20 | 21 | AnyTransportBag(const AnyTransportBag&) = delete; 22 | AnyTransportBag(AnyTransportBag&&) noexcept = delete; 23 | AnyTransportBag& operator=(const AnyTransportBag&) = delete; 24 | AnyTransportBag& operator=(AnyTransportBag&&) noexcept = delete; 25 | 26 | virtual Transport& getTransport() const = 0; 27 | 28 | protected: 29 | AnyTransportBag() = default; 30 | ~AnyTransportBag() = default; 31 | 32 | }; // AnyTransportBag 33 | 34 | #endif // ANY_TRANSPORT_BAG_HPP_INCLUDED 35 | -------------------------------------------------------------------------------- /libcyphal_demo/src/application.cpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #include "application.hpp" 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include // for std::stoul 22 | 23 | namespace 24 | { 25 | 26 | constexpr std::size_t HeapSize = 128ULL * 1024ULL; 27 | alignas(O1HEAP_ALIGNMENT) std::array s_heap_arena{}; // NOLINT 28 | 29 | constexpr std::size_t BlockHeapSize = 128ULL * 1024ULL; 30 | alignas(O1HEAP_ALIGNMENT) std::array s_block_heap_arena{}; // NOLINT 31 | 32 | } // namespace 33 | 34 | Application::Application(const char* const root_path) 35 | : o1_heap_mr_{s_heap_arena} 36 | , o1_block_heap_mr_{s_block_heap_arena} 37 | , media_block_mr_{o1_block_heap_mr_} 38 | , storage_{root_path} 39 | , registry_{o1_heap_mr_} 40 | , regs_{o1_heap_mr_, registry_, media_block_mr_} 41 | { 42 | cetl::pmr::set_default_resource(&o1_heap_mr_); 43 | 44 | load(storage_, registry_); 45 | 46 | // Maybe override some of the registry values with environment variables. 47 | // 48 | auto iface_params = getIfaceParams(); 49 | if (const auto* const iface_addresses_str = std::getenv("CYPHAL__UDP__IFACE")) 50 | { 51 | iface_params.udp_iface.value() = iface_addresses_str; 52 | } 53 | if (const auto* const iface_mtu_str = std::getenv("CYPHAL__UDP__MTU")) 54 | { 55 | iface_params.udp_mtu.value()[0] = static_cast(std::stoul(iface_mtu_str)); 56 | } 57 | if (const auto* const iface_addresses_str = std::getenv("CYPHAL__CAN__IFACE")) 58 | { 59 | iface_params.can_iface.value() = iface_addresses_str; 60 | } 61 | if (const auto* const iface_mtu_str = std::getenv("CYPHAL__CAN__MTU")) 62 | { 63 | iface_params.can_mtu.value()[0] = static_cast(std::stoul(iface_mtu_str)); 64 | } 65 | auto node_params = getNodeParams(); 66 | if (const auto* const node_id_str = std::getenv("CYPHAL__NODE__ID")) 67 | { 68 | node_params.id.value()[0] = static_cast(std::stoul(node_id_str)); 69 | } 70 | } 71 | 72 | Application::~Application() 73 | { 74 | save(storage_, registry_); 75 | 76 | const auto o1_diag = o1_heap_mr_.queryDiagnostics(); 77 | std::cout << "O(1) Heap diagnostics:" << "\n" 78 | << " capacity=" << o1_diag.capacity << "\n" 79 | << " allocated=" << o1_diag.allocated << "\n" 80 | << " peak_allocated=" << o1_diag.peak_allocated << "\n" 81 | << " peak_request_size=" << o1_diag.peak_request_size << "\n" 82 | << " oom_count=" << o1_diag.oom_count << "\n"; 83 | 84 | const auto blk_diag = media_block_mr_.queryDiagnostics(); 85 | std::cout << "Media block memory diagnostics:" << "\n" 86 | << " capacity=" << blk_diag.capacity << "\n" 87 | << " allocated=" << blk_diag.allocated << "\n" 88 | << " peak_allocated=" << blk_diag.peak_allocated << "\n" 89 | << " block_size=" << blk_diag.block_size << "\n" 90 | << " oom_count=" << blk_diag.oom_count << "\n"; 91 | 92 | cetl::pmr::set_default_resource(cetl::pmr::new_delete_resource()); 93 | } 94 | 95 | /// Returns the 128-bit unique-ID of the local node. This value is used in `uavcan.node.GetInfo.Response`. 96 | /// 97 | Application::UniqueId Application::getUniqueId() 98 | { 99 | UniqueId out_unique_id = {}; 100 | 101 | const auto result = storage_.get(".unique_id", out_unique_id); 102 | if (cetl::get_if(&result) != nullptr) 103 | { 104 | std::random_device rd; // Seed for the random number engine 105 | std::mt19937 gen{rd()}; // Mersenne Twister engine 106 | std::uniform_int_distribution dis{0, 255}; // Distribution range for bytes 107 | 108 | // Populate the default; it is only used at the first run. 109 | for (auto& b : out_unique_id) 110 | { 111 | b = dis(gen); 112 | } 113 | 114 | (void) storage_.put(".unique_id", out_unique_id); 115 | } 116 | 117 | return out_unique_id; 118 | } 119 | 120 | Application::Regs::Value Application::Regs::getSysInfoMemBlock() const 121 | { 122 | Value value{{&o1_heap_mr_}}; 123 | auto& uint64s = value.set_natural64(); 124 | 125 | const auto diagnostics = media_block_mr_.queryDiagnostics(); 126 | uint64s.value.reserve(5); // NOLINT five fields gonna push 127 | uint64s.value.push_back(diagnostics.capacity); 128 | uint64s.value.push_back(diagnostics.allocated); 129 | uint64s.value.push_back(diagnostics.peak_allocated); 130 | uint64s.value.push_back(diagnostics.block_size); 131 | uint64s.value.push_back(diagnostics.oom_count); 132 | 133 | return value; 134 | } 135 | 136 | Application::Regs::Value Application::Regs::getSysInfoMemGeneral() const 137 | { 138 | Value value{{&o1_heap_mr_}}; 139 | auto& uint64s = value.set_natural64(); 140 | 141 | const auto diagnostics = o1_heap_mr_.queryDiagnostics(); 142 | uint64s.value.reserve(5); // NOLINT five fields gonna push 143 | uint64s.value.push_back(diagnostics.capacity); 144 | uint64s.value.push_back(diagnostics.allocated); 145 | uint64s.value.push_back(diagnostics.peak_allocated); 146 | uint64s.value.push_back(diagnostics.peak_request_size); 147 | uint64s.value.push_back(diagnostics.oom_count); 148 | 149 | return value; 150 | } 151 | -------------------------------------------------------------------------------- /libcyphal_demo/src/exec_cmd_provider.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef COMMAND_PROVIDER_HPP_INCLUDED 8 | #define COMMAND_PROVIDER_HPP_INCLUDED 9 | 10 | #include "libcyphal/application/node.hpp" 11 | #include "libcyphal/presentation/presentation.hpp" 12 | #include "libcyphal/presentation/server.hpp" 13 | #include "libcyphal/time_provider.hpp" 14 | #include "libcyphal/transport/types.hpp" 15 | #include "libcyphal/types.hpp" 16 | 17 | #include 18 | 19 | #include 20 | 21 | #include 22 | #include 23 | 24 | /// Defines 'ExecuteCommand' provider component for the application node. 25 | /// 26 | /// Internally, it uses the 'ExecuteCommand' service server to handle incoming requests. 27 | /// 28 | /// No Sonar cpp:S4963 'The "Rule-of-Zero" should be followed' 29 | /// b/c we do directly handle resources here (namely capturing of `this` in the request callback). 30 | /// 31 | template 32 | class ExecCmdProvider // NOSONAR cpp:S4963 33 | { 34 | public: 35 | using Service = uavcan::node::ExecuteCommand_1_3; 36 | using Server = libcyphal::presentation::ServiceServer; 37 | 38 | /// Defines the response type for the ExecuteCommand provider. 39 | /// 40 | using Response = Service::Response; 41 | 42 | /// Defines the request type for the ExecuteCommand provider. 43 | /// 44 | using Request = Service::Request; 45 | 46 | /// Typealias to the request command type of the ExecuteCommand provider. 47 | /// 48 | /// `std::uint16_t` is used as the command type. 49 | // Use `Request::COMMAND_XXX` constants to access standard command values. 50 | /// 51 | using Command = Request::_traits_::TypeOf::command; 52 | 53 | /// Factory method to create a ExecuteCommand instance. 54 | /// 55 | /// @param node The application layer node instance. In use to access heartbeat producer. 56 | /// @param presentation The presentation layer instance. In use to create 'ExecuteCommand' service server. 57 | /// @param time_provider The time provider - in use to calculate RPC call deadlines. 58 | /// @return The ExecuteCommand provider instance or a failure. 59 | /// 60 | static auto make(libcyphal::application::Node& node, 61 | libcyphal::presentation::Presentation& presentation, 62 | libcyphal::ITimeProvider& time_provider) 63 | -> libcyphal::Expected 64 | { 65 | auto maybe_srv = presentation.makeServer(); 66 | if (auto* const failure = cetl::get_if(&maybe_srv)) 67 | { 68 | return std::move(*failure); 69 | } 70 | 71 | return Derived{node, presentation, time_provider, cetl::get(std::move(maybe_srv))}; 72 | } 73 | 74 | ExecCmdProvider(ExecCmdProvider&& other) noexcept 75 | : alloc_{other.alloc_} 76 | , server_{std::move(other.server_)} 77 | , response_timeout_{other.response_timeout_} 78 | { 79 | // We have to set up request callback again (b/c it captures its own `this` pointer), 80 | setupOnRequestCallback(); 81 | } 82 | 83 | virtual ~ExecCmdProvider() = default; 84 | 85 | ExecCmdProvider(const ExecCmdProvider&) = delete; 86 | ExecCmdProvider& operator=(const ExecCmdProvider&) = delete; 87 | ExecCmdProvider& operator=(ExecCmdProvider&&) noexcept = delete; 88 | 89 | /// Sets the response transmission timeout (default is 1s). 90 | /// 91 | /// @param timeout Duration of the response transmission timeout. Applied for the next response transmission. 92 | /// 93 | void setResponseTimeout(const libcyphal::Duration& timeout) noexcept 94 | { 95 | response_timeout_ = timeout; 96 | } 97 | 98 | /// Handles incoming command requests. 99 | /// 100 | /// This method is called by the service server when a new request is received. 101 | /// The user should override the method to handle custom commands. 102 | /// If the method returns `false`, the server will respond with a `STATUS_BAD_COMMAND` status. 103 | /// 104 | /// @param command The command to be executed. 105 | /// @param parameter The command parameter. 106 | /// @param metadata The transport RX metadata. 107 | /// @param response The response to be sent back to the requester. 108 | /// 109 | virtual bool onCommand(const Request::_traits_::TypeOf::command command, 110 | const cetl::string_view parameter, 111 | const libcyphal::transport::ServiceRxMetadata& metadata, 112 | Response& response) noexcept 113 | { 114 | (void) command; 115 | (void) parameter; 116 | (void) metadata; 117 | (void) response; 118 | 119 | return false; 120 | } 121 | 122 | protected: 123 | ExecCmdProvider(const libcyphal::presentation::Presentation& presentation, Server&& server) 124 | : alloc_{&presentation.memory()} 125 | , server_{std::move(server)} 126 | , response_timeout_{std::chrono::seconds{1}} 127 | { 128 | setupOnRequestCallback(); 129 | } 130 | 131 | private: 132 | void setupOnRequestCallback() 133 | { 134 | server_.setOnRequestCallback([this](const auto& arg, auto continuation) { 135 | // 136 | Response response{alloc_}; 137 | if (!onCommand(arg.request.command, makeStringView(arg.request.parameter), arg.metadata, response)) 138 | { 139 | response.status = Response::STATUS_BAD_COMMAND; 140 | } 141 | 142 | // There is nothing we can do about possible continuation failures - we just ignore them. 143 | // TODO: Introduce error handler at the node level. 144 | (void) continuation(arg.approx_now + response_timeout_, response); 145 | }); 146 | } 147 | 148 | /// Makes a new string view from request string parameter. 149 | /// 150 | static cetl::string_view makeStringView(const Request::_traits_::TypeOf::parameter& container) 151 | { 152 | // No Lint and Sonar cpp:S3630 "reinterpret_cast" should not be used" b/c we need to access container raw data. 153 | // NOLINTNEXTLINE(*-pro-type-reinterpret-cast) 154 | return {reinterpret_cast(container.data()), container.size()}; // NOSONAR 155 | } 156 | 157 | // MARK: Data members: 158 | 159 | Response::allocator_type alloc_; 160 | Server server_; 161 | libcyphal::Duration response_timeout_; 162 | 163 | }; // ExecCmdProvider 164 | 165 | #endif // COMMAND_PROVIDER_HPP_INCLUDED 166 | -------------------------------------------------------------------------------- /libcyphal_demo/src/no_cpp_heap.cpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #if (__cplusplus >= CETL_CPP_STANDARD_17) 14 | # include 15 | #endif 16 | 17 | // Disable std c++ heap allocations. 18 | // In this demo we gonna use only stack and PMR allocations. 19 | // 20 | extern void* operator new(std::size_t) 21 | { 22 | std::cerr << "operator `new(size_t)` has been called"; 23 | std::exit(1); 24 | } 25 | extern void operator delete(void*) noexcept 26 | { 27 | std::cerr << "operator `delete(void*)` has been called"; 28 | std::exit(1); 29 | } 30 | 31 | #if (__cplusplus >= CETL_CPP_STANDARD_17) 32 | 33 | extern void* operator new(std::size_t, std::align_val_t) 34 | { 35 | std::cerr << "operator `new(size_t, align_val_t)` has been called"; 36 | std::exit(1); 37 | } 38 | extern void operator delete(void*, std::align_val_t) noexcept 39 | { 40 | std::cerr << "operator `delete(void*, align_val_t)` has been called"; 41 | std::exit(1); 42 | } 43 | 44 | #endif // (__cplusplus >= CETL_CPP_STANDARD_17) 45 | -------------------------------------------------------------------------------- /libcyphal_demo/src/platform/block_memory_resource.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef PLATFORM_BLOCK_MEMORY_RESOURCE_HPP 8 | #define PLATFORM_BLOCK_MEMORY_RESOURCE_HPP 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace platform 21 | { 22 | 23 | /// Implements a C++17 PMR memory resource that uses a pool of pre-allocated blocks. 24 | /// 25 | class BlockMemoryResource final : public cetl::pmr::memory_resource 26 | { 27 | public: 28 | struct Diagnostics final 29 | { 30 | std::size_t capacity; 31 | std::size_t allocated; 32 | std::size_t peak_allocated; 33 | std::size_t block_size; 34 | std::uint64_t oom_count; 35 | 36 | }; // Diagnostics 37 | 38 | explicit BlockMemoryResource(cetl::pmr::memory_resource& memory) 39 | : memory_{memory} 40 | , pool_ptr_{nullptr, {&memory, 0U}} 41 | { 42 | } 43 | 44 | ~BlockMemoryResource() override = default; 45 | 46 | BlockMemoryResource(BlockMemoryResource&&) = delete; 47 | BlockMemoryResource(const BlockMemoryResource&) = delete; 48 | BlockMemoryResource& operator=(BlockMemoryResource&&) = delete; 49 | BlockMemoryResource& operator=(const BlockMemoryResource&) = delete; 50 | 51 | /// Initializes the memory pool. 52 | /// 53 | /// Normally, such setup functionality of this method should be in the constructor. 54 | /// However, we need to pass this block memory resource to a media first. 55 | /// Then the media will be passed to the transport creation, 56 | /// and only then we can set up this block memory resource according to MTU of the transport 57 | /// and total number of redundant media. To break this dependency cycle, 58 | /// we have to separate the setup of the block memory resource from its construction. 59 | /// 60 | void setup(const std::size_t pool_size, const std::size_t block_size, const std::size_t alignment) 61 | { 62 | CETL_DEBUG_ASSERT(!pool_ptr_, ""); 63 | CETL_DEBUG_ASSERT(block_size > 0U, ""); 64 | CETL_DEBUG_ASSERT(pool_size >= alignment, ""); 65 | CETL_DEBUG_ASSERT(alignment && !(alignment & (alignment - 1)), "Should be a power of 2"); 66 | 67 | pool_ptr_ = PoolPtr{memory_.allocate(pool_size), {&memory_, pool_size}}; 68 | if (!pool_ptr_) 69 | { 70 | CETL_DEBUG_ASSERT(false, "Failed to allocate memory pool"); 71 | return; 72 | } 73 | 74 | // Internal implementation requires at least `alignof(void*)` alignment - 75 | // b/c we link free blocks in the pool using pointers. 76 | alignment_ = std::max(alignment, alignof(void*)); 77 | 78 | // Enforce alignment and padding of the input arguments. We may waste some space as a result. 79 | const std::size_t bs = (block_size + alignment_ - 1U) & ~(alignment_ - 1U); 80 | std::size_t sz_bytes = pool_size; 81 | auto* ptr = reinterpret_cast(pool_ptr_.get()); // NOLINT 82 | while ((reinterpret_cast(ptr) % alignment_) != 0U) // NOLINT 83 | { 84 | ptr++; // NOLINT 85 | if (sz_bytes > 0U) 86 | { 87 | sz_bytes--; 88 | } 89 | } 90 | 91 | block_size_ = bs; 92 | block_count_ = sz_bytes / bs; 93 | head_ = reinterpret_cast(ptr); // NOLINT 94 | 95 | for (std::size_t i = 0U; i < block_count_; i++) 96 | { 97 | *reinterpret_cast(ptr + (i * bs)) = // NOLINT 98 | ((i + 1U) < block_count_) ? static_cast(ptr + ((i + 1U) * bs)) : nullptr; // NOLINT 99 | } 100 | } 101 | 102 | Diagnostics queryDiagnostics() const noexcept 103 | { 104 | return {block_count_, used_blocks_, used_blocks_peak_, block_size_, oom_count_}; 105 | } 106 | 107 | protected: 108 | // MARK: cetl::pmr::memory_resource 109 | 110 | void* do_allocate(std::size_t size_bytes, std::size_t alignment) override // NOLINT 111 | { 112 | if (alignment > alignment_) 113 | { 114 | #if defined(__cpp_exceptions) 115 | throw std::bad_alloc(); 116 | #else 117 | return nullptr; 118 | #endif 119 | } 120 | 121 | // C++ standard (basic.stc.dynamic.allocation) requires that a memory allocation never returns 122 | // nullptr (even for the zero). 123 | // So, we have to handle this case explicitly by returning a non-null pointer to an empty storage. 124 | if (size_bytes == 0U) 125 | { 126 | return empty_storage_.data(); 127 | } 128 | 129 | void* out = nullptr; 130 | request_count_++; 131 | if (size_bytes <= block_size_) 132 | { 133 | out = static_cast(head_); // NOLINT 134 | if (head_ != nullptr) 135 | { 136 | head_ = static_cast(*head_); // NOLINT 137 | used_blocks_++; 138 | used_blocks_peak_ = std::max(used_blocks_, used_blocks_peak_); 139 | } 140 | } 141 | if (out == nullptr) 142 | { 143 | oom_count_++; 144 | } 145 | return out; 146 | } 147 | 148 | void do_deallocate(void* ptr, std::size_t size_bytes, std::size_t alignment) override // NOLINT 149 | { 150 | CETL_DEBUG_ASSERT((nullptr != ptr) || (0U == size_bytes), ""); 151 | CETL_DEBUG_ASSERT(size_bytes <= block_size_, ""); 152 | (void) size_bytes; 153 | (void) alignment; 154 | 155 | // See `do_allocate` special case for zero bytes. 156 | if (ptr == empty_storage_.data()) 157 | { 158 | CETL_DEBUG_ASSERT(0U == size_bytes, ""); 159 | return; 160 | } 161 | 162 | if (ptr != nullptr) 163 | { 164 | *static_cast(ptr) = static_cast(head_); 165 | head_ = static_cast(ptr); 166 | CETL_DEBUG_ASSERT(used_blocks_ > 0U, ""); 167 | used_blocks_--; 168 | } 169 | } 170 | 171 | bool do_is_equal(const cetl::pmr::memory_resource& other) const noexcept override 172 | { 173 | return this == &other; 174 | } 175 | 176 | private: 177 | using PoolPtr = std::unique_ptr>; 178 | 179 | cetl::pmr::memory_resource& memory_; 180 | PoolPtr pool_ptr_; 181 | std::size_t alignment_{0U}; 182 | void** head_{nullptr}; 183 | std::size_t block_count_{0U}; 184 | std::size_t block_size_{0U}; 185 | std::size_t used_blocks_{0U}; 186 | std::size_t used_blocks_peak_{0U}; 187 | std::size_t request_count_{0U}; 188 | std::size_t oom_count_{0U}; 189 | 190 | // See `do_allocate` special case for zero bytes. 191 | // Note that we still need at least one byte - b/c `std::array<..., 0>::data()` returns `nullptr`. 192 | std::array empty_storage_{}; 193 | 194 | }; // BlockMemoryResource 195 | 196 | } // namespace platform 197 | 198 | #endif // PLATFORM_BLOCK_MEMORY_RESOURCE_HPP 199 | -------------------------------------------------------------------------------- /libcyphal_demo/src/platform/bsd/kqueue_single_threaded_executor.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef PLATFORM_BSD_KQUEUE_SINGLE_THREADED_EXECUTOR_HPP_INCLUDED 8 | #define PLATFORM_BSD_KQUEUE_SINGLE_THREADED_EXECUTOR_HPP_INCLUDED 9 | 10 | #include "platform/posix/posix_executor_extension.hpp" 11 | #include "platform/posix/posix_platform_error.hpp" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | namespace platform 38 | { 39 | namespace bsd 40 | { 41 | 42 | /// @brief Defines BSD Linux platform-specific single-threaded executor based on `kqueue` mechanism. 43 | /// 44 | class KqueueSingleThreadedExecutor final : public libcyphal::platform::SingleThreadedExecutor, 45 | public posix::IPosixExecutorExtension 46 | { 47 | public: 48 | KqueueSingleThreadedExecutor() 49 | : kqueuefd_{::kqueue()} 50 | , total_awaitables_{0} 51 | { 52 | } 53 | 54 | KqueueSingleThreadedExecutor(const KqueueSingleThreadedExecutor&) = delete; 55 | KqueueSingleThreadedExecutor(KqueueSingleThreadedExecutor&&) noexcept = delete; 56 | KqueueSingleThreadedExecutor& operator=(const KqueueSingleThreadedExecutor&) = delete; 57 | KqueueSingleThreadedExecutor& operator=(KqueueSingleThreadedExecutor&&) noexcept = delete; 58 | 59 | ~KqueueSingleThreadedExecutor() override 60 | { 61 | if (kqueuefd_ >= 0) 62 | { 63 | ::close(kqueuefd_); 64 | } 65 | } 66 | 67 | CETL_NODISCARD cetl::optional pollAwaitableResourcesFor( 68 | const cetl::optional timeout) const override 69 | { 70 | CETL_DEBUG_ASSERT((total_awaitables_ > 0) || timeout, 71 | "Infinite timeout without awaitables means that we will sleep forever."); 72 | 73 | if (total_awaitables_ == 0) 74 | { 75 | if (!timeout) 76 | { 77 | return libcyphal::ArgumentError{}; 78 | } 79 | 80 | std::this_thread::sleep_for(*timeout); 81 | return cetl::nullopt; 82 | } 83 | 84 | // Convert libcyphal timeout (if any) to the `struct timespec` timeout in ns. 85 | // Any possible negative timeout will be treated as zero (return immediately from the `::kevent`). 86 | // 87 | struct timespec timeout_spec 88 | {}; 89 | const struct timespec* timeout_spec_ptr = nullptr; 90 | if (timeout) 91 | { 92 | using PollDuration = std::chrono::nanoseconds; 93 | using TimeoutNsType = decltype(timespec::tv_nsec); 94 | 95 | // Fill nanoseconds part of the timeout spec taking into account the maximum possible value. 96 | // 97 | timeout_spec.tv_nsec = static_cast( // 98 | std::max(static_cast(0), 99 | std::min(std::chrono::duration_cast(*timeout).count(), 100 | static_cast(std::numeric_limits::max())))); 101 | 102 | timeout_spec_ptr = &timeout_spec; 103 | } 104 | 105 | std::array evs{}; 106 | const int kqueue_result = ::kevent(kqueuefd_, nullptr, 0, evs.data(), evs.size(), timeout_spec_ptr); 107 | if (kqueue_result < 0) 108 | { 109 | const auto err = errno; 110 | if (err == EINTR) 111 | { 112 | // Normally, we would just retry a system call (`::kevent`), 113 | // but we need updated timeout (from the main loop). 114 | return cetl::nullopt; 115 | } 116 | return libcyphal::transport::PlatformError{posix::PosixPlatformError{err}}; 117 | } 118 | if (kqueue_result == 0) 119 | { 120 | return cetl::nullopt; 121 | } 122 | const auto kqueue_nfds = static_cast(kqueue_result); 123 | 124 | const auto now_time = now(); 125 | for (std::size_t index = 0; index < kqueue_nfds; ++index) 126 | { 127 | const KEvent& ev = evs[index]; 128 | if (auto* const cb_interface = static_cast(ev.udata)) 129 | { 130 | cb_interface->schedule(Callback::Schedule::Once{now_time}); 131 | } 132 | } 133 | 134 | return cetl::nullopt; 135 | } 136 | 137 | protected: 138 | // MARK: - IPosixExecutorExtension 139 | 140 | CETL_NODISCARD Callback::Any registerAwaitableCallback(Callback::Function&& function, 141 | const Trigger::Variant& trigger) override 142 | { 143 | AwaitableNode new_cb_node{*this, std::move(function)}; 144 | 145 | cetl::visit( // 146 | cetl::make_overloaded( 147 | [&new_cb_node](const Trigger::Readable& readable) { 148 | // 149 | new_cb_node.setup(readable.fd, EVFILT_READ); 150 | }, 151 | [&new_cb_node](const Trigger::Writable& writable) { 152 | // 153 | new_cb_node.setup(writable.fd, EVFILT_WRITE); 154 | }), 155 | trigger); 156 | 157 | insertCallbackNode(new_cb_node); 158 | return {std::move(new_cb_node)}; 159 | } 160 | 161 | // MARK: - RTTI 162 | 163 | CETL_NODISCARD void* _cast_(const cetl::type_id& id) & noexcept override 164 | { 165 | if (id == IPosixExecutorExtension::_get_type_id_()) 166 | { 167 | return static_cast(this); 168 | } 169 | return Base::_cast_(id); 170 | } 171 | CETL_NODISCARD const void* _cast_(const cetl::type_id& id) const& noexcept override 172 | { 173 | if (id == IPosixExecutorExtension::_get_type_id_()) 174 | { 175 | return static_cast(this); 176 | } 177 | return Base::_cast_(id); 178 | } 179 | 180 | private: 181 | using KEvent = struct kevent; 182 | using Base = SingleThreadedExecutor; 183 | using Self = KqueueSingleThreadedExecutor; 184 | 185 | /// No Sonar cpp:S4963 b/c `AwaitableNode` supports move operation. 186 | /// 187 | class AwaitableNode final : public CallbackNode // NOSONAR cpp:S4963 188 | { 189 | public: 190 | AwaitableNode(Self& executor, Callback::Function&& function) 191 | : CallbackNode{executor, std::move(function)} 192 | , fd_{-1} 193 | , filter_{0} 194 | { 195 | } 196 | 197 | ~AwaitableNode() override 198 | { 199 | if (fd_ >= 0) 200 | { 201 | KEvent ev{}; 202 | EV_SET(&ev, fd_, filter_, EV_DELETE, 0, 0, 0); 203 | ::kevent(getExecutor().kqueuefd_, &ev, 1, nullptr, 0, nullptr); 204 | getExecutor().total_awaitables_--; 205 | } 206 | } 207 | 208 | AwaitableNode(AwaitableNode&& other) noexcept 209 | : CallbackNode(std::move(static_cast(other))) 210 | , fd_{std::exchange(other.fd_, -1)} 211 | , filter_{std::exchange(other.filter_, 0)} 212 | { 213 | if (fd_ >= 0) 214 | { 215 | KEvent ev{}; 216 | EV_SET(&ev, fd_, filter_, EV_DELETE, 0, 0, 0); 217 | ::kevent(getExecutor().kqueuefd_, &ev, 1, nullptr, 0, nullptr); 218 | EV_SET(&ev, fd_, filter_, EV_ADD, 0, 0, this); 219 | ::kevent(getExecutor().kqueuefd_, &ev, 1, nullptr, 0, nullptr); 220 | } 221 | } 222 | 223 | AwaitableNode(const AwaitableNode&) = delete; 224 | AwaitableNode& operator=(const AwaitableNode&) = delete; 225 | AwaitableNode& operator=(AwaitableNode&& other) noexcept = delete; 226 | 227 | int fd() const noexcept 228 | { 229 | return fd_; 230 | } 231 | 232 | std::int16_t filter() const noexcept 233 | { 234 | return filter_; 235 | } 236 | 237 | void setup(const int fd, const std::int16_t filter) noexcept 238 | { 239 | CETL_DEBUG_ASSERT(fd >= 0, ""); 240 | CETL_DEBUG_ASSERT(filter != 0, ""); 241 | 242 | fd_ = fd; 243 | filter_ = filter; 244 | 245 | getExecutor().total_awaitables_++; 246 | KEvent ev{}; 247 | EV_SET(&ev, fd, filter_, EV_ADD, 0, 0, this); 248 | ::kevent(getExecutor().kqueuefd_, &ev, 1, nullptr, 0, nullptr); 249 | } 250 | 251 | private: 252 | Self& getExecutor() noexcept 253 | { 254 | // No lint b/c we know for sure that the executor is of type `Self`. 255 | // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) 256 | return static_cast(executor()); 257 | } 258 | 259 | // MARK: Data members: 260 | 261 | int fd_; 262 | std::int16_t filter_; 263 | 264 | }; // AwaitableNode 265 | 266 | // MARK: - Data members: 267 | 268 | static constexpr int MaxEvents = 16; 269 | 270 | int kqueuefd_; 271 | std::size_t total_awaitables_; 272 | 273 | }; // KqueueSingleThreadedExecutor 274 | 275 | } // namespace bsd 276 | } // namespace platform 277 | 278 | #endif // PLATFORM_BSD_KQUEUE_SINGLE_THREADED_EXECUTOR_HPP_INCLUDED 279 | -------------------------------------------------------------------------------- /libcyphal_demo/src/platform/common_helpers.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef PLATFORM_COMMON_HELPERS_HPP_INCLUDED 8 | #define PLATFORM_COMMON_HELPERS_HPP_INCLUDED 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifdef __linux__ 17 | #include 18 | #endif 19 | 20 | #include 21 | 22 | namespace platform 23 | { 24 | 25 | struct CommonHelpers 26 | { 27 | struct Printers 28 | { 29 | static cetl::string_view describeError(const libcyphal::ArgumentError&) 30 | { 31 | return "ArgumentError"; 32 | } 33 | static cetl::string_view describeError(const libcyphal::MemoryError&) 34 | { 35 | return "MemoryError"; 36 | } 37 | static cetl::string_view describeError(const libcyphal::transport::AnonymousError&) 38 | { 39 | return "AnonymousError"; 40 | } 41 | static cetl::string_view describeError(const libcyphal::transport::CapacityError&) 42 | { 43 | return "CapacityError"; 44 | } 45 | static cetl::string_view describeError(const libcyphal::transport::AlreadyExistsError&) 46 | { 47 | return "AlreadyExistsError"; 48 | } 49 | static cetl::string_view describeError(const libcyphal::transport::PlatformError& error) 50 | { 51 | return "PlatformError"; 52 | } 53 | 54 | static cetl::string_view describeAnyFailure(const libcyphal::transport::AnyFailure& failure) 55 | { 56 | return cetl::visit([](const auto& error) { return describeError(error); }, failure); 57 | } 58 | }; 59 | 60 | #ifdef __linux__ 61 | 62 | struct Can 63 | { 64 | static cetl::optional transientErrorReporter( 65 | libcyphal::transport::can::ICanTransport::TransientErrorReport::Variant& report_var) 66 | { 67 | using Report = libcyphal::transport::can::ICanTransport::TransientErrorReport; 68 | 69 | cetl::visit( // 70 | cetl::make_overloaded( 71 | [](const Report::CanardTxPush& report) { 72 | std::cerr << "Failed to push TX frame to canard " 73 | << "(mediaIdx=" << static_cast(report.media_index) << ").\n" 74 | << Printers::describeAnyFailure(report.failure) << "\n"; 75 | }, 76 | [](const Report::CanardRxAccept& report) { 77 | std::cerr << "Failed to accept RX frame at canard " 78 | << "(mediaIdx=" << static_cast(report.media_index) << ").\n" 79 | << Printers::describeAnyFailure(report.failure) << "\n"; 80 | }, 81 | [](const Report::MediaPop& report) { 82 | std::cerr << "Failed to pop frame from media " 83 | << "(mediaIdx=" << static_cast(report.media_index) << ").\n" 84 | << Printers::describeAnyFailure(report.failure) << "\n"; 85 | }, 86 | [](const Report::ConfigureMedia& report) { 87 | std::cerr << "Failed to configure CAN.\n" 88 | << Printers::describeAnyFailure(report.failure) << "\n"; 89 | }, 90 | [](const Report::MediaConfig& report) { 91 | std::cerr << "Failed to configure media " 92 | << "(mediaIdx=" << static_cast(report.media_index) << ").\n" 93 | << Printers::describeAnyFailure(report.failure) << "\n"; 94 | }, 95 | [](const Report::MediaPush& report) { 96 | std::cerr << "Failed to push frame to media " 97 | << "(mediaIdx=" << static_cast(report.media_index) << ").\n" 98 | << Printers::describeAnyFailure(report.failure) << "\n"; 99 | }), 100 | report_var); 101 | 102 | // "swallows" all transient failures, thus giving a chance 103 | // to other redundant media interfaces to continue. 104 | return cetl::nullopt; 105 | } 106 | 107 | }; // Can 108 | 109 | #endif // __linux__ 110 | 111 | struct Udp 112 | { 113 | static cetl::optional transientErrorReporter( 114 | libcyphal::transport::udp::IUdpTransport::TransientErrorReport::Variant& report_var) 115 | { 116 | using Report = libcyphal::transport::udp::IUdpTransport::TransientErrorReport; 117 | 118 | cetl::visit( // 119 | cetl::make_overloaded( 120 | [](const Report::UdpardTxPublish& report) { 121 | std::cerr << "Failed to TX message frame to udpard " 122 | << "(mediaIdx=" << static_cast(report.media_index) << ").\n" 123 | << Printers::describeAnyFailure(report.failure) << "\n"; 124 | }, 125 | [](const Report::UdpardTxRequest& report) { 126 | std::cerr << "Failed to TX request frame to udpard " 127 | << "(mediaIdx=" << static_cast(report.media_index) << ").\n" 128 | << Printers::describeAnyFailure(report.failure) << "\n"; 129 | }, 130 | [](const Report::UdpardTxRespond& report) { 131 | std::cerr << "Failed to TX response frame to udpard " 132 | << "(mediaIdx=" << static_cast(report.media_index) << ").\n" 133 | << Printers::describeAnyFailure(report.failure) << "\n"; 134 | }, 135 | [](const Report::UdpardRxMsgReceive& report) { 136 | std::cerr << "Failed to accept RX message frame at udpard " 137 | << Printers::describeAnyFailure(report.failure) << "\n"; 138 | }, 139 | [](const Report::UdpardRxSvcReceive& report) { 140 | std::cerr << "Failed to accept RX service frame at udpard " 141 | << "(mediaIdx=" << static_cast(report.media_index) << ").\n" 142 | << Printers::describeAnyFailure(report.failure) << "\n"; 143 | }, 144 | [](const Report::MediaMakeRxSocket& report) { 145 | std::cerr << "Failed to make RX socket " << "(mediaIdx=" << static_cast(report.media_index) 146 | << ").\n" 147 | << Printers::describeAnyFailure(report.failure) << "\n"; 148 | }, 149 | [](const Report::MediaMakeTxSocket& report) { 150 | std::cerr << "Failed to make TX socket " << "(mediaIdx=" << static_cast(report.media_index) 151 | << ").\n" 152 | << Printers::describeAnyFailure(report.failure) << "\n"; 153 | }, 154 | [](const Report::MediaTxSocketSend& report) { 155 | std::cerr << "Failed to TX frame to socket " 156 | << "(mediaIdx=" << static_cast(report.media_index) << ").\n" 157 | << Printers::describeAnyFailure(report.failure) << "\n"; 158 | }, 159 | [](const Report::MediaRxSocketReceive& report) { 160 | std::cerr << "Failed to RX frame from socket " 161 | << "(mediaIdx=" << static_cast(report.media_index) << ").\n" 162 | << Printers::describeAnyFailure(report.failure) << "\n"; 163 | }), 164 | report_var); 165 | 166 | // "swallows" all transient failures, thus giving a chance 167 | // to other redundant media interfaces to continue. 168 | return cetl::nullopt; 169 | } 170 | 171 | }; // Udp 172 | 173 | }; // CommonHelpers 174 | 175 | } // namespace platform 176 | 177 | #endif // PLATFORM_COMMON_HELPERS_HPP_INCLUDED 178 | -------------------------------------------------------------------------------- /libcyphal_demo/src/platform/defines.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef PLATFORM_DEFINES_HPP_INCLUDED 8 | #define PLATFORM_DEFINES_HPP_INCLUDED 9 | 10 | #ifdef PLATFORM_OS_TYPE_BSD 11 | # include "bsd/kqueue_single_threaded_executor.hpp" 12 | #else 13 | # include "linux/epoll_single_threaded_executor.hpp" 14 | #endif 15 | 16 | namespace platform 17 | { 18 | 19 | #ifdef PLATFORM_OS_TYPE_BSD 20 | using SingleThreadedExecutor = bsd::KqueueSingleThreadedExecutor; 21 | #else 22 | using SingleThreadedExecutor = Linux::EpollSingleThreadedExecutor; 23 | #endif 24 | 25 | } // namespace platform 26 | 27 | #endif // PLATFORM_DEFINES_HPP_INCLUDED 28 | -------------------------------------------------------------------------------- /libcyphal_demo/src/platform/linux/epoll_single_threaded_executor.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef PLATFORM_LINUX_EPOLL_SINGLE_THREADED_EXECUTOR_HPP_INCLUDED 8 | #define PLATFORM_LINUX_EPOLL_SINGLE_THREADED_EXECUTOR_HPP_INCLUDED 9 | 10 | #include "platform/posix/posix_executor_extension.hpp" 11 | #include "platform/posix/posix_platform_error.hpp" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | namespace platform 36 | { 37 | namespace Linux 38 | { 39 | 40 | /// @brief Defines Linux platform-specific single-threaded executor based on `epoll` mechanism. 41 | /// 42 | class EpollSingleThreadedExecutor final : public libcyphal::platform::SingleThreadedExecutor, 43 | public posix::IPosixExecutorExtension 44 | { 45 | public: 46 | EpollSingleThreadedExecutor() 47 | : epollfd_{::epoll_create1(0)} 48 | , total_awaitables_{0} 49 | { 50 | } 51 | 52 | EpollSingleThreadedExecutor(const EpollSingleThreadedExecutor&) = delete; 53 | EpollSingleThreadedExecutor(EpollSingleThreadedExecutor&&) noexcept = delete; 54 | EpollSingleThreadedExecutor& operator=(const EpollSingleThreadedExecutor&) = delete; 55 | EpollSingleThreadedExecutor& operator=(EpollSingleThreadedExecutor&&) noexcept = delete; 56 | 57 | ~EpollSingleThreadedExecutor() override 58 | { 59 | if (epollfd_ >= 0) 60 | { 61 | ::close(epollfd_); 62 | } 63 | } 64 | 65 | CETL_NODISCARD cetl::optional pollAwaitableResourcesFor( 66 | const cetl::optional timeout) const override 67 | { 68 | CETL_DEBUG_ASSERT((total_awaitables_ > 0) || timeout, 69 | "Infinite timeout without awaitables means that we will sleep forever."); 70 | 71 | if (total_awaitables_ == 0) 72 | { 73 | if (!timeout) 74 | { 75 | return libcyphal::ArgumentError{}; 76 | } 77 | 78 | std::this_thread::sleep_for(*timeout); 79 | return cetl::nullopt; 80 | } 81 | 82 | // Make sure that timeout is within the range of `::epoll_wait()`'s `int` timeout parameter. 83 | // Any possible negative timeout will be treated as zero (return immediately from the `::epoll_wait`). 84 | // 85 | int clamped_timeout_ms = -1; // "infinite" timeout 86 | if (timeout) 87 | { 88 | using PollDuration = std::chrono::milliseconds; 89 | 90 | clamped_timeout_ms = static_cast( // 91 | std::max(static_cast(0), 92 | std::min(std::chrono::duration_cast(*timeout).count(), 93 | static_cast(std::numeric_limits::max())))); 94 | } 95 | 96 | std::array evs{}; 97 | const int epoll_result = ::epoll_wait(epollfd_, evs.data(), evs.size(), clamped_timeout_ms); 98 | if (epoll_result < 0) 99 | { 100 | const auto err = errno; 101 | if (err == EINTR) 102 | { 103 | // Normally, we would just retry a system call (`::epoll_wait`), 104 | // but we need updated timeout (from the main loop). 105 | return cetl::nullopt; 106 | } 107 | return libcyphal::transport::PlatformError{posix::PosixPlatformError{err}}; 108 | } 109 | if (epoll_result == 0) 110 | { 111 | return cetl::nullopt; 112 | } 113 | const auto epoll_nfds = static_cast(epoll_result); 114 | 115 | const auto now_time = now(); 116 | for (std::size_t index = 0; index < epoll_nfds; ++index) 117 | { 118 | const epoll_event& ev = evs[index]; 119 | if (auto* const cb_interface = static_cast(ev.data.ptr)) 120 | { 121 | cb_interface->schedule(Callback::Schedule::Once{now_time}); 122 | } 123 | } 124 | 125 | return cetl::nullopt; 126 | } 127 | 128 | protected: 129 | // MARK: - IPosixExecutorExtension 130 | 131 | CETL_NODISCARD Callback::Any registerAwaitableCallback(Callback::Function&& function, 132 | const Trigger::Variant& trigger) override 133 | { 134 | AwaitableNode new_cb_node{*this, std::move(function)}; 135 | 136 | cetl::visit( // 137 | cetl::make_overloaded( 138 | [&new_cb_node](const Trigger::Readable& readable) { 139 | // 140 | new_cb_node.setup(readable.fd, EPOLLIN); 141 | }, 142 | [&new_cb_node](const Trigger::Writable& writable) { 143 | // 144 | new_cb_node.setup(writable.fd, EPOLLOUT); 145 | }), 146 | trigger); 147 | 148 | insertCallbackNode(new_cb_node); 149 | return {std::move(new_cb_node)}; 150 | } 151 | 152 | // MARK: - RTTI 153 | 154 | CETL_NODISCARD void* _cast_(const cetl::type_id& id) & noexcept override 155 | { 156 | if (id == IPosixExecutorExtension::_get_type_id_()) 157 | { 158 | return static_cast(this); 159 | } 160 | return Base::_cast_(id); 161 | } 162 | CETL_NODISCARD const void* _cast_(const cetl::type_id& id) const& noexcept override 163 | { 164 | if (id == IPosixExecutorExtension::_get_type_id_()) 165 | { 166 | return static_cast(this); 167 | } 168 | return Base::_cast_(id); 169 | } 170 | 171 | private: 172 | using Base = SingleThreadedExecutor; 173 | using Self = EpollSingleThreadedExecutor; 174 | 175 | /// No Sonar cpp:S4963 b/c `AwaitableNode` supports move operation. 176 | /// 177 | class AwaitableNode final : public CallbackNode // NOSONAR cpp:S4963 178 | { 179 | public: 180 | AwaitableNode(Self& executor, Callback::Function&& function) 181 | : CallbackNode{executor, std::move(function)} 182 | , fd_{-1} 183 | , events_{0} 184 | { 185 | } 186 | 187 | ~AwaitableNode() override 188 | { 189 | if (fd_ >= 0) 190 | { 191 | ::epoll_ctl(getExecutor().epollfd_, EPOLL_CTL_DEL, fd_, nullptr); 192 | getExecutor().total_awaitables_--; 193 | } 194 | } 195 | 196 | AwaitableNode(AwaitableNode&& other) noexcept 197 | : CallbackNode(std::move(other)) 198 | , fd_{std::exchange(other.fd_, -1)} 199 | , events_{std::exchange(other.events_, 0)} 200 | { 201 | if (fd_ >= 0) 202 | { 203 | ::epoll_event ev{events_, {this}}; 204 | ::epoll_ctl(getExecutor().epollfd_, EPOLL_CTL_MOD, fd_, &ev); 205 | } 206 | } 207 | 208 | AwaitableNode(const AwaitableNode&) = delete; 209 | AwaitableNode& operator=(const AwaitableNode&) = delete; 210 | AwaitableNode& operator=(AwaitableNode&& other) noexcept = delete; 211 | 212 | int fd() const noexcept 213 | { 214 | return fd_; 215 | } 216 | 217 | std::uint32_t events() const noexcept 218 | { 219 | return events_; 220 | } 221 | 222 | void setup(const int fd, const std::uint32_t events) noexcept 223 | { 224 | CETL_DEBUG_ASSERT(fd >= 0, ""); 225 | CETL_DEBUG_ASSERT(events != 0, ""); 226 | 227 | fd_ = fd; 228 | events_ = events; 229 | 230 | getExecutor().total_awaitables_++; 231 | ::epoll_event ev{events_, {this}}; 232 | ::epoll_ctl(getExecutor().epollfd_, EPOLL_CTL_ADD, fd_, &ev); 233 | } 234 | 235 | private: 236 | Self& getExecutor() noexcept 237 | { 238 | // No lint b/c we know for sure that the executor is of type `Self`. 239 | // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) 240 | return static_cast(executor()); 241 | } 242 | 243 | // MARK: Data members: 244 | 245 | int fd_; 246 | std::uint32_t events_; 247 | 248 | }; // AwaitableNode 249 | 250 | // MARK: - Data members: 251 | 252 | static constexpr int MaxEpollEvents = 16; 253 | 254 | int epollfd_; 255 | std::size_t total_awaitables_; 256 | 257 | }; // LinuxSingleThreadedExecutor 258 | 259 | } // namespace Linux 260 | } // namespace platform 261 | 262 | #endif // PLATFORM_LINUX_EPOLL_SINGLE_THREADED_EXECUTOR_HPP_INCLUDED 263 | -------------------------------------------------------------------------------- /libcyphal_demo/src/platform/o1_heap_memory_resource.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef PLATFORM_O1_HEAP_MEMORY_RESOURCE_HPP 8 | #define PLATFORM_O1_HEAP_MEMORY_RESOURCE_HPP 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace platform 21 | { 22 | 23 | /// Implements a C++17 PMR memory resource that uses the O(1) heap. 24 | /// 25 | class O1HeapMemoryResource final : public cetl::pmr::memory_resource 26 | { 27 | public: 28 | template 29 | explicit O1HeapMemoryResource(std::array& heap_arena) 30 | : o1_heap_{o1heapInit(heap_arena.data(), heap_arena.size())} 31 | { 32 | CETL_DEBUG_ASSERT(o1_heap_ != nullptr, ""); 33 | } 34 | 35 | O1HeapDiagnostics queryDiagnostics() const noexcept 36 | { 37 | return o1heapGetDiagnostics(o1_heap_); 38 | } 39 | 40 | private: 41 | // MARK: cetl::pmr::memory_resource 42 | 43 | void* do_allocate(std::size_t size_bytes, std::size_t alignment) override // NOLINT 44 | { 45 | if (alignment > alignof(std::max_align_t)) 46 | { 47 | #if defined(__cpp_exceptions) 48 | throw std::bad_alloc(); 49 | #else 50 | return nullptr; 51 | #endif 52 | } 53 | 54 | // О1Heap is a C-library, and it follows `malloc` convention of returning nullptr on requesting zero bytes. 55 | // In contrast, C++ standard (basic.stc.dynamic.allocation) requires that a memory allocation never returns 56 | // nullptr (even for the zero). 57 | // So, we have to handle this case explicitly by returning a non-null pointer to an empty storage. 58 | if (size_bytes == 0) 59 | { 60 | return empty_storage_.data(); 61 | } 62 | 63 | return o1heapAllocate(o1_heap_, size_bytes); 64 | } 65 | 66 | void do_deallocate(void* ptr, std::size_t size_bytes, std::size_t alignment) override // NOLINT 67 | { 68 | CETL_DEBUG_ASSERT((nullptr != ptr) || (0 == size_bytes), ""); 69 | (void) size_bytes; 70 | (void) alignment; 71 | 72 | // See `do_allocate` special case for zero bytes. 73 | if (ptr == empty_storage_.data()) 74 | { 75 | CETL_DEBUG_ASSERT(0 == size_bytes, ""); 76 | return; 77 | } 78 | 79 | o1heapFree(o1_heap_, ptr); 80 | } 81 | 82 | #if (__cplusplus < CETL_CPP_STANDARD_17) 83 | 84 | void* do_reallocate(void* ptr, 85 | std::size_t old_size_bytes, 86 | std::size_t new_size_bytes, // NOLINT 87 | std::size_t alignment) override 88 | { 89 | CETL_DEBUG_ASSERT((nullptr != ptr) || (0 == old_size_bytes), ""); 90 | (void) alignment; 91 | 92 | // See `do_allocate` special case for zero bytes. 93 | if (new_size_bytes == 0) 94 | { 95 | if (ptr != empty_storage_.data()) 96 | { 97 | o1heapFree(o1_heap_, ptr); 98 | } 99 | return empty_storage_.data(); 100 | } 101 | 102 | if (auto* const new_ptr = o1heapAllocate(o1_heap_, new_size_bytes)) 103 | { 104 | std::memmove(new_ptr, ptr, std::min(old_size_bytes, new_size_bytes)); 105 | 106 | // See `do_allocate` special case for zero bytes. 107 | if (ptr != empty_storage_.data()) 108 | { 109 | o1heapFree(o1_heap_, ptr); 110 | } 111 | 112 | return new_ptr; 113 | } 114 | return nullptr; 115 | } 116 | 117 | #endif 118 | 119 | bool do_is_equal(const cetl::pmr::memory_resource& rhs) const noexcept override 120 | { 121 | return (&rhs == this); 122 | } 123 | 124 | // MARK: Data members: 125 | 126 | O1HeapInstance* o1_heap_; 127 | 128 | // See `do_allocate` special case for zero bytes. 129 | // Note that we still need at least one byte - b/c `std::array<..., 0>::data()` returns `nullptr`. 130 | std::array empty_storage_{}; 131 | 132 | }; // O1HeapMemoryResource 133 | 134 | } // namespace platform 135 | 136 | #endif // PLATFORM_O1_HEAP_MEMORY_RESOURCE_HPP 137 | -------------------------------------------------------------------------------- /libcyphal_demo/src/platform/posix/posix_executor_extension.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef PLATFORM_POSIX_EXECUTOR_EXTENSION_HPP_INCLUDED 8 | #define PLATFORM_POSIX_EXECUTOR_EXTENSION_HPP_INCLUDED 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace platform 19 | { 20 | namespace posix 21 | { 22 | 23 | class IPosixExecutorExtension 24 | { 25 | // FFE3771E-7962-4CEA-ACA6-ED7895699080 26 | using TypeIdType = cetl:: 27 | // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) 28 | type_id_type<0xFF, 0xE3, 0x77, 0x1E, 0x79, 0x62, 0x4C, 0xEA, 0xAC, 0xA6, 0xED, 0x78, 0x95, 0x69, 0x90, 0x80>; 29 | 30 | public: 31 | IPosixExecutorExtension(const IPosixExecutorExtension&) = delete; 32 | IPosixExecutorExtension(IPosixExecutorExtension&&) noexcept = delete; 33 | IPosixExecutorExtension& operator=(const IPosixExecutorExtension&) = delete; 34 | IPosixExecutorExtension& operator=(IPosixExecutorExtension&&) noexcept = delete; 35 | 36 | struct Trigger 37 | { 38 | struct Readable 39 | { 40 | int fd; 41 | }; 42 | struct Writable 43 | { 44 | int fd; 45 | }; 46 | 47 | using Variant = cetl::variant; 48 | }; 49 | 50 | CETL_NODISCARD virtual libcyphal::IExecutor::Callback::Any registerAwaitableCallback( 51 | libcyphal::IExecutor::Callback::Function&& function, 52 | const Trigger::Variant& trigger) = 0; 53 | 54 | using PollFailure = cetl::variant; 55 | 56 | CETL_NODISCARD virtual cetl::optional pollAwaitableResourcesFor( 57 | const cetl::optional timeout) const = 0; 58 | 59 | // MARK: RTTI 60 | 61 | static constexpr cetl::type_id _get_type_id_() noexcept 62 | { 63 | return cetl::type_id_type_value(); 64 | } 65 | 66 | protected: 67 | IPosixExecutorExtension() = default; 68 | ~IPosixExecutorExtension() = default; 69 | 70 | }; // IPosixExecutorExtension 71 | 72 | } // namespace posix 73 | } // namespace platform 74 | 75 | #endif // PLATFORM_POSIX_EXECUTOR_EXTENSION_HPP_INCLUDED 76 | -------------------------------------------------------------------------------- /libcyphal_demo/src/platform/posix/posix_platform_error.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef PLATFORM_POSIX_PLATFORM_ERROR_HPP_INCLUDED 8 | #define PLATFORM_POSIX_PLATFORM_ERROR_HPP_INCLUDED 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | namespace platform 16 | { 17 | namespace posix 18 | { 19 | 20 | class PosixPlatformError final : public libcyphal::transport::IPlatformError 21 | { 22 | public: 23 | explicit PosixPlatformError(const int err) 24 | : code_{err} 25 | { 26 | CETL_DEBUG_ASSERT(err > 0, ""); 27 | } 28 | virtual ~PosixPlatformError() = default; 29 | PosixPlatformError(const PosixPlatformError&) = default; 30 | PosixPlatformError(PosixPlatformError&&) noexcept = default; 31 | PosixPlatformError& operator=(const PosixPlatformError&) = default; 32 | PosixPlatformError& operator=(PosixPlatformError&&) noexcept = default; 33 | 34 | // MARK: IPlatformError 35 | 36 | /// Gets platform-specific error code. 37 | /// 38 | /// In this case, the error code is the POSIX error code (aka `errno`). 39 | /// 40 | std::uint32_t code() const noexcept override 41 | { 42 | return static_cast(code_); 43 | } 44 | 45 | private: 46 | int code_; 47 | 48 | }; // PosixPlatformError 49 | 50 | } // namespace posix 51 | } // namespace platform 52 | 53 | #endif // PLATFORM_POSIX_PLATFORM_ERROR_HPP_INCLUDED 54 | -------------------------------------------------------------------------------- /libcyphal_demo/src/platform/posix/udp/udp_media.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef PLATFORM_POSIX_UDP_MEDIA_HPP_INCLUDED 8 | #define PLATFORM_POSIX_UDP_MEDIA_HPP_INCLUDED 9 | 10 | #include "platform/string.hpp" 11 | #include "udp_sockets.hpp" 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | 25 | namespace platform 26 | { 27 | namespace posix 28 | { 29 | 30 | class UdpMedia final : public libcyphal::transport::udp::IMedia 31 | { 32 | public: 33 | UdpMedia(cetl::pmr::memory_resource& general_mr, 34 | libcyphal::IExecutor& executor, 35 | const cetl::string_view iface_address, 36 | cetl::pmr::memory_resource& tx_mr) 37 | : general_mr_{general_mr} 38 | , executor_{executor} 39 | , iface_address_{iface_address} 40 | , iface_mtu_{UDPARD_MTU_DEFAULT} 41 | , tx_mr_{tx_mr} 42 | { 43 | } 44 | 45 | ~UdpMedia() = default; 46 | 47 | UdpMedia(const UdpMedia&) = delete; 48 | UdpMedia& operator=(const UdpMedia&) = delete; 49 | UdpMedia* operator=(UdpMedia&&) noexcept = delete; 50 | 51 | UdpMedia(UdpMedia&& other) noexcept 52 | : general_mr_{other.general_mr_} 53 | , executor_{other.executor_} 54 | , iface_address_{other.iface_address_} 55 | , iface_mtu_{other.iface_mtu_} 56 | , tx_mr_{other.tx_mr_} 57 | { 58 | } 59 | 60 | void setAddress(const cetl::string_view iface_address) 61 | { 62 | iface_address_ = iface_address; 63 | } 64 | 65 | void setMtu(const std::size_t mtu) 66 | { 67 | iface_mtu_ = mtu; 68 | } 69 | 70 | private: 71 | // MARK: - IMedia 72 | 73 | MakeTxSocketResult::Type makeTxSocket() override 74 | { 75 | return UdpTxSocket::make(general_mr_, executor_, iface_address_.data(), iface_mtu_); 76 | } 77 | 78 | MakeRxSocketResult::Type makeRxSocket(const libcyphal::transport::udp::IpEndpoint& multicast_endpoint) override 79 | { 80 | return UdpRxSocket::make(general_mr_, executor_, iface_address_.data(), multicast_endpoint); 81 | } 82 | 83 | cetl::pmr::memory_resource& getTxMemoryResource() override 84 | { 85 | return tx_mr_; 86 | } 87 | 88 | // MARK: Data members: 89 | 90 | cetl::pmr::memory_resource& general_mr_; 91 | libcyphal::IExecutor& executor_; 92 | String<64> iface_address_; 93 | std::size_t iface_mtu_; 94 | cetl::pmr::memory_resource& tx_mr_; 95 | 96 | }; // UdpMedia 97 | 98 | // MARK: - 99 | 100 | struct UdpMediaCollection 101 | { 102 | UdpMediaCollection(cetl::pmr::memory_resource& general_mr, 103 | libcyphal::IExecutor& executor, 104 | cetl::pmr::memory_resource& tx_mr) 105 | : media_array_{{// 106 | {general_mr, executor, "", tx_mr}, 107 | {general_mr, executor, "", tx_mr}, 108 | {general_mr, executor, "", tx_mr}}} 109 | { 110 | } 111 | 112 | void parse(const cetl::string_view iface_addresses, const std::size_t iface_mtu) 113 | { 114 | // Split addresses by spaces. 115 | // 116 | std::size_t index = 0; 117 | std::size_t curr = 0; 118 | while ((curr != cetl::string_view::npos) && (index < MaxUdpMedia)) 119 | { 120 | const auto next = iface_addresses.find(' ', curr); 121 | const auto iface_address = iface_addresses.substr(curr, next - curr); 122 | if (!iface_address.empty()) 123 | { 124 | auto& media = media_array_[index]; // NOLINT 125 | 126 | media.setAddress(iface_address); 127 | media.setMtu(iface_mtu); 128 | index++; 129 | } 130 | 131 | curr = std::max(next + 1, next); // `+1` to skip the space 132 | } 133 | 134 | media_ifaces_ = {}; 135 | for (std::size_t i = 0; i < index; i++) 136 | { 137 | media_ifaces_[i] = &media_array_[i]; // NOLINT 138 | } 139 | } 140 | 141 | cetl::span span() 142 | { 143 | return {media_ifaces_.data(), media_ifaces_.size()}; 144 | } 145 | 146 | std::size_t count() const 147 | { 148 | return std::count_if(media_ifaces_.cbegin(), media_ifaces_.cend(), [](const auto* iface) { 149 | // 150 | return iface != nullptr; 151 | }); 152 | } 153 | 154 | private: 155 | static constexpr std::size_t MaxUdpMedia = 3; 156 | 157 | std::array media_array_; 158 | std::array media_ifaces_{}; 159 | 160 | }; // UdpMediaCollection 161 | 162 | } // namespace posix 163 | } // namespace platform 164 | 165 | #endif // PLATFORM_POSIX_UDP_MEDIA_HPP_INCLUDED 166 | -------------------------------------------------------------------------------- /libcyphal_demo/src/platform/posix/udp/udp_sockets.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef PLATFORM_POSIX_UDP_SOCKETS_HPP_INCLUDED 8 | #define PLATFORM_POSIX_UDP_SOCKETS_HPP_INCLUDED 9 | 10 | #include "platform/posix/posix_executor_extension.hpp" 11 | #include "platform/posix/posix_platform_error.hpp" 12 | #include "udp.h" 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | namespace platform 32 | { 33 | namespace posix 34 | { 35 | 36 | class UdpTxSocket final : public libcyphal::transport::udp::ITxSocket 37 | { 38 | public: 39 | CETL_NODISCARD static libcyphal::transport::udp::IMedia::MakeTxSocketResult::Type make( 40 | cetl::pmr::memory_resource& memory, 41 | libcyphal::IExecutor& executor, 42 | const char* const iface_address, 43 | const std::size_t iface_mtu) 44 | { 45 | UDPTxHandle handle{-1}; 46 | const auto result = ::udpTxInit(&handle, ::udpParseIfaceAddress(iface_address)); 47 | if (result < 0) 48 | { 49 | return libcyphal::transport::PlatformError{PosixPlatformError{-result}}; 50 | } 51 | 52 | auto tx_socket = libcyphal::makeUniquePtr(memory, executor, handle, iface_mtu); 53 | if (tx_socket == nullptr) 54 | { 55 | ::udpTxClose(&handle); 56 | return libcyphal::MemoryError{}; 57 | } 58 | 59 | return tx_socket; 60 | } 61 | 62 | UdpTxSocket(libcyphal::IExecutor& executor, UDPTxHandle udp_handle, const std::size_t iface_mtu) 63 | : udp_handle_{udp_handle} 64 | , executor_{executor} 65 | , iface_mtu_{iface_mtu} 66 | { 67 | CETL_DEBUG_ASSERT(udp_handle_.fd >= 0, ""); 68 | } 69 | 70 | ~UdpTxSocket() 71 | { 72 | ::udpTxClose(&udp_handle_); 73 | } 74 | 75 | UdpTxSocket(const UdpTxSocket&) = delete; 76 | UdpTxSocket(UdpTxSocket&&) noexcept = delete; 77 | UdpTxSocket& operator=(const UdpTxSocket&) = delete; 78 | UdpTxSocket& operator=(UdpTxSocket&&) noexcept = delete; 79 | 80 | private: 81 | // MARK: ITxSocket 82 | 83 | std::size_t getMtu() const noexcept override 84 | { 85 | return iface_mtu_; 86 | } 87 | 88 | SendResult::Type send(const libcyphal::TimePoint, 89 | const libcyphal::transport::udp::IpEndpoint multicast_endpoint, 90 | const std::uint8_t dscp, 91 | const libcyphal::transport::PayloadFragments payload_fragments) override 92 | { 93 | CETL_DEBUG_ASSERT(udp_handle_.fd >= 0, ""); 94 | CETL_DEBUG_ASSERT(payload_fragments.size() == 1, ""); 95 | 96 | const std::int16_t result = ::udpTxSend(&udp_handle_, 97 | multicast_endpoint.ip_address, 98 | multicast_endpoint.udp_port, 99 | dscp, 100 | payload_fragments[0].size(), 101 | payload_fragments[0].data()); 102 | if (result < 0) 103 | { 104 | return libcyphal::transport::PlatformError{PosixPlatformError{-result}}; 105 | } 106 | 107 | return SendResult::Success{result == 1}; 108 | } 109 | 110 | CETL_NODISCARD libcyphal::IExecutor::Callback::Any registerCallback( 111 | libcyphal::IExecutor::Callback::Function&& function) override 112 | { 113 | auto* const posix_executor_ext = cetl::rtti_cast(&executor_); 114 | if (nullptr == posix_executor_ext) 115 | { 116 | return {}; 117 | } 118 | 119 | CETL_DEBUG_ASSERT(udp_handle_.fd >= 0, ""); 120 | return posix_executor_ext->registerAwaitableCallback(std::move(function), 121 | IPosixExecutorExtension::Trigger::Writable{ 122 | udp_handle_.fd}); 123 | } 124 | 125 | // MARK: Data members: 126 | 127 | UDPTxHandle udp_handle_; 128 | libcyphal::IExecutor& executor_; 129 | std::size_t iface_mtu_; 130 | 131 | }; // UdpTxSocket 132 | 133 | // MARK: - 134 | 135 | class UdpRxSocket final : public libcyphal::transport::udp::IRxSocket 136 | { 137 | public: 138 | CETL_NODISCARD static libcyphal::transport::udp::IMedia::MakeRxSocketResult::Type make( 139 | cetl::pmr::memory_resource& memory, 140 | libcyphal::IExecutor& executor, 141 | const std::string& address, 142 | const libcyphal::transport::udp::IpEndpoint& endpoint) 143 | { 144 | UDPRxHandle handle{-1}; 145 | const auto result = 146 | ::udpRxInit(&handle, ::udpParseIfaceAddress(address.c_str()), endpoint.ip_address, endpoint.udp_port); 147 | if (result < 0) 148 | { 149 | return libcyphal::transport::PlatformError{PosixPlatformError{-result}}; 150 | } 151 | 152 | auto rx_socket = libcyphal::makeUniquePtr(memory, executor, handle, memory); 153 | if (rx_socket == nullptr) 154 | { 155 | ::udpRxClose(&handle); 156 | return libcyphal::MemoryError{}; 157 | } 158 | 159 | return rx_socket; 160 | } 161 | 162 | UdpRxSocket(libcyphal::IExecutor& executor, UDPRxHandle udp_handle, cetl::pmr::memory_resource& memory) 163 | : udp_handle_{udp_handle} 164 | , executor_{executor} 165 | , memory_{memory} 166 | { 167 | CETL_DEBUG_ASSERT(udp_handle_.fd >= 0, ""); 168 | } 169 | 170 | ~UdpRxSocket() 171 | { 172 | ::udpRxClose(&udp_handle_); 173 | } 174 | 175 | UdpRxSocket(const UdpRxSocket&) = delete; 176 | UdpRxSocket(UdpRxSocket&&) noexcept = delete; 177 | UdpRxSocket& operator=(const UdpRxSocket&) = delete; 178 | UdpRxSocket& operator=(UdpRxSocket&&) noexcept = delete; 179 | 180 | private: 181 | static constexpr std::size_t BufferSize = 2000; 182 | 183 | // MARK: IRxSocket 184 | 185 | CETL_NODISCARD ReceiveResult::Type receive() override 186 | { 187 | CETL_DEBUG_ASSERT(udp_handle_.fd >= 0, ""); 188 | 189 | // Current Udpard api limitation is not allowing to pass bigger buffer than actual data size is. 190 | // Hence, we need temp buffer on stack, and then memory copying. 191 | // TODO: Eliminate tmp buffer and memmove when https://github.com/OpenCyphal/libudpard/issues/58 is resolved. 192 | // 193 | std::array buffer{}; 194 | std::size_t inout_size = buffer.size(); 195 | const std::int16_t result = ::udpRxReceive(&udp_handle_, &inout_size, buffer.data()); 196 | if (result < 0) 197 | { 198 | return libcyphal::transport::PlatformError{PosixPlatformError{-result}}; 199 | } 200 | if (result == 0) 201 | { 202 | return cetl::nullopt; 203 | } 204 | // 205 | auto* const allocated_buffer = memory_.allocate(inout_size); 206 | if (nullptr == allocated_buffer) 207 | { 208 | return libcyphal::MemoryError{}; 209 | } 210 | (void) std::memmove(allocated_buffer, buffer.data(), inout_size); 211 | 212 | return ReceiveResult::Metadata{executor_.now(), 213 | {static_cast(allocated_buffer), 214 | libcyphal::PmrRawBytesDeleter{inout_size, &memory_}}}; 215 | } 216 | 217 | CETL_NODISCARD libcyphal::IExecutor::Callback::Any registerCallback( 218 | libcyphal::IExecutor::Callback::Function&& function) override 219 | { 220 | auto* const posix_executor_ext = cetl::rtti_cast(&executor_); 221 | if (nullptr == posix_executor_ext) 222 | { 223 | return {}; 224 | } 225 | 226 | CETL_DEBUG_ASSERT(udp_handle_.fd >= 0, ""); 227 | return posix_executor_ext->registerAwaitableCallback(std::move(function), 228 | IPosixExecutorExtension::Trigger::Readable{ 229 | udp_handle_.fd}); 230 | } 231 | 232 | // MARK: Data members: 233 | 234 | UDPRxHandle udp_handle_; 235 | libcyphal::IExecutor& executor_; 236 | cetl::pmr::memory_resource& memory_; 237 | 238 | }; // UdpRxSocket 239 | 240 | } // namespace posix 241 | } // namespace platform 242 | 243 | #endif // PLATFORM_POSIX_UDP_SOCKETS_HPP_INCLUDED 244 | -------------------------------------------------------------------------------- /libcyphal_demo/src/platform/storage.hpp: -------------------------------------------------------------------------------- 1 | /// @copyright 2 | /// Copyright (C) OpenCyphal Development Team 3 | /// Copyright Amazon.com Inc. or its affiliates. 4 | /// SPDX-License-Identifier: MIT 5 | 6 | #ifndef PLATFORM_STORAGE_HPP_INCLUDED 7 | #define PLATFORM_STORAGE_HPP_INCLUDED 8 | 9 | #include "string.hpp" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace platform 25 | { 26 | namespace storage 27 | { 28 | 29 | /// Defines an example key-value storage implementation. 30 | /// 31 | /// This implementation uses the file system to store key-value pairs. 32 | /// C API (instead of C++ file streams) is used to ensure that there is no c++ heap involved. 33 | /// IO error handling is missing for brevity - the goal is to have primitive platform storage. 34 | /// 35 | class KeyValue final : public libcyphal::platform::storage::IKeyValue 36 | { 37 | using Error = libcyphal::platform::storage::Error; 38 | 39 | public: 40 | explicit KeyValue(const char* const root_path) 41 | : root_path_{root_path} 42 | { 43 | if ((mkdir(root_path, 0755) != 0) && (errno != EEXIST)) // NOLINT 44 | { 45 | std::cerr << "Error making folder: '" << root_path_ << "'.\n"; 46 | std::cerr << "Error: " << std::strerror(errno) << "\n"; 47 | } 48 | } 49 | 50 | // MARK: - IKeyValue 51 | 52 | auto get(const cetl::string_view key, 53 | const cetl::span data) const -> libcyphal::Expected override 54 | { 55 | const auto file_path = makeFilePath(key); 56 | FILE* const file = std::fopen(file_path.c_str(), "rb"); // NOLINT 57 | if (file == nullptr) 58 | { 59 | return Error::Existence; 60 | } 61 | 62 | const auto data_size = std::fread(data.data(), 1, data.size(), file); 63 | (void) std::fclose(file); // NOLINT 64 | 65 | return data_size; 66 | } 67 | 68 | auto put(const cetl::string_view key, const cetl::span data) // 69 | -> cetl::optional override 70 | { 71 | const auto file_path = makeFilePath(key); 72 | FILE* const file = std::fopen(file_path.c_str(), "wb"); // NOLINT 73 | if (file == nullptr) 74 | { 75 | return Error::Existence; 76 | } 77 | 78 | (void) std::fwrite(data.data(), 1, data.size(), file); // NOLINT 79 | (void) std::fclose(file); // NOLINT 80 | 81 | return cetl::nullopt; 82 | } 83 | 84 | auto drop(const cetl::string_view key) -> cetl::optional override 85 | { 86 | const auto file_path = makeFilePath(key); 87 | if ((std::remove(file_path.c_str()) != 0) && (errno != ENOENT)) 88 | { 89 | std::cerr << "Error removing file: '" << file_path.c_str() << "'.\n"; 90 | std::cerr << "Error: " << std::strerror(errno) << "\n"; 91 | return Error::IO; 92 | } 93 | return cetl::nullopt; 94 | } 95 | 96 | private: 97 | static constexpr std::size_t MaxPathLen = 64; 98 | using StringPath = String; 99 | 100 | /// In practice, the keys could be hashed, so it won't be necessary to deal with directory nesting. 101 | /// This is fine b/c we don't need key listing, and so we don't have to retain the key names. 102 | /// 103 | /// But with the below implementation, users can easily remove a single value by deleting 104 | /// the corresponding file on their system (under `/tmp/org.opencyphal.demos.libcyphal/` folder) - 105 | /// with hashes it would be harder to figure out which file to delete. 106 | /// 107 | StringPath makeFilePath(const cetl::string_view key) const 108 | { 109 | StringPath file_path{root_path_}; 110 | file_path << "/" << key; 111 | return file_path; 112 | } 113 | 114 | const cetl::string_view root_path_; 115 | 116 | }; // KeyValue 117 | 118 | } // namespace storage 119 | } // namespace platform 120 | 121 | #endif // PLATFORM_STORAGE_HPP_INCLUDED 122 | -------------------------------------------------------------------------------- /libcyphal_demo/src/transport_bag_can.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef TRANSPORT_BAG_CAN_HPP_INCLUDED 8 | #define TRANSPORT_BAG_CAN_HPP_INCLUDED 9 | 10 | #include "any_transport_bag.hpp" 11 | #include "application.hpp" 12 | #include "platform/block_memory_resource.hpp" 13 | #include "platform/common_helpers.hpp" 14 | #include "platform/linux/can/can_media.hpp" 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | /// Holds (internally) instance of the CAN transport and its media (if any). 31 | /// 32 | class TransportBagCan final : public AnyTransportBag 33 | { 34 | /// Defines private specification for making interface unique ptr. 35 | /// 36 | struct Spec 37 | { 38 | explicit Spec() = default; 39 | }; 40 | 41 | public: 42 | Transport& getTransport() const override 43 | { 44 | CETL_DEBUG_ASSERT(transport_, ""); 45 | return *transport_; 46 | } 47 | 48 | static Ptr make(cetl::pmr::memory_resource& general_mr, 49 | libcyphal::IExecutor& executor, 50 | platform::BlockMemoryResource& media_block_mr, 51 | const Application::IfaceParams& params) 52 | { 53 | if (params.can_iface.value().empty()) 54 | { 55 | return nullptr; 56 | } 57 | 58 | auto any_transport_bag = cetl::pmr::InterfaceFactory::make_unique( // 59 | cetl::pmr::polymorphic_allocator{&general_mr}, 60 | Spec{}, 61 | general_mr, 62 | executor, 63 | media_block_mr); 64 | if (!any_transport_bag) 65 | { 66 | return nullptr; 67 | } 68 | auto& bag = static_cast(*any_transport_bag); // NOLINT 69 | 70 | bag.media_collection_.parse(params.can_iface.value(), params.can_mtu.value()[0]); 71 | auto maybe_can_transport = makeTransport({general_mr}, executor, bag.media_collection_.span(), TxQueueCapacity); 72 | if (const auto* failure = cetl::get_if(&maybe_can_transport)) 73 | { 74 | std::cerr << "❌ Failed to create CAN transport (iface='" 75 | << static_cast(params.can_iface.value()) << "').\n"; 76 | return nullptr; 77 | } 78 | bag.transport_ = cetl::get>( // 79 | std::move(maybe_can_transport)); 80 | 81 | std::cout << "CAN Iface : '" << params.can_iface.value().c_str() << "'\n"; 82 | const std::size_t mtu = bag.transport_->getProtocolParams().mtu_bytes; 83 | std::cout << "Iface MTU : " << mtu << "\n"; 84 | 85 | // Canard allocates memory for raw bytes block only, so there is no alignment requirement. 86 | constexpr std::size_t block_alignment = 1; 87 | const std::size_t block_size = mtu; 88 | const std::size_t pool_size = bag.media_collection_.count() * TxQueueCapacity * block_size; 89 | bag.media_block_mr_.setup(pool_size, block_size, block_alignment); 90 | 91 | // To support redundancy (multiple homogeneous interfaces), it's important to have a non-default 92 | // handler which "swallows" expected transient failures (by returning `nullopt` result). 93 | // Otherwise, the default Cyphal behavior will fail/interrupt current and future transfers 94 | // if some of its media encounter transient failures - thus breaking the whole redundancy goal, 95 | // namely, maintain communication if at least one of the interfaces is still up and running. 96 | // 97 | bag.transport_->setTransientErrorHandler([](auto&) { return cetl::nullopt; }); 98 | // bag.transport_->setTransientErrorHandler(platform::CommonHelpers::Can::transientErrorReporter); 99 | 100 | return any_transport_bag; 101 | } 102 | 103 | TransportBagCan(Spec, 104 | cetl::pmr::memory_resource& general_mr, 105 | libcyphal::IExecutor& executor, 106 | platform::BlockMemoryResource& media_block_mr) 107 | : general_mr_{general_mr} 108 | , executor_{executor} 109 | , media_block_mr_{media_block_mr} 110 | , media_collection_{general_mr, executor, media_block_mr} 111 | { 112 | } 113 | 114 | private: 115 | using TransportPtr = libcyphal::UniquePtr; 116 | 117 | // Our current max `SerializationBufferSizeBytes` is 515 bytes (for `uavcan.register.Access.Request.1.0`) 118 | // Assuming CAN classic presentation MTU of 7 bytes (plus a bit of overhead like CRC and stuff), 119 | // let's calculate the required TX queue capacity, and make it twice to accommodate 2 such messages. 120 | static constexpr std::size_t TxQueueCapacity = 2 * (515U + 8U) / 7U; 121 | 122 | cetl::pmr::memory_resource& general_mr_; 123 | libcyphal::IExecutor& executor_; 124 | platform::BlockMemoryResource& media_block_mr_; 125 | platform::Linux::CanMediaCollection media_collection_; 126 | TransportPtr transport_; 127 | 128 | }; // TransportBagCan 129 | 130 | #endif // TRANSPORT_BAG_CAN_HPP_INCLUDED 131 | -------------------------------------------------------------------------------- /libcyphal_demo/src/transport_bag_udp.hpp: -------------------------------------------------------------------------------- 1 | // This software is distributed under the terms of the MIT License. 2 | // Copyright (C) OpenCyphal Development Team 3 | // Copyright Amazon.com Inc. or its affiliates. 4 | // SPDX-License-Identifier: MIT 5 | // Author: Sergei Shirokov 6 | 7 | #ifndef TRANSPORT_BAG_UDP_HPP_INCLUDED 8 | #define TRANSPORT_BAG_UDP_HPP_INCLUDED 9 | 10 | #include "any_transport_bag.hpp" 11 | #include "application.hpp" 12 | #include "platform/block_memory_resource.hpp" 13 | #include "platform/common_helpers.hpp" 14 | #include "platform/posix/udp/udp_media.hpp" 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | /// Holds (internally) instance of the UDP transport and its media (if any). 30 | /// 31 | class TransportBagUdp final : public AnyTransportBag 32 | { 33 | /// Defines private specification for making interface unique ptr. 34 | /// 35 | struct Spec : libcyphal::detail::UniquePtrSpec 36 | { 37 | explicit Spec() = default; 38 | }; 39 | 40 | public: 41 | Transport& getTransport() const override 42 | { 43 | CETL_DEBUG_ASSERT(transport_, ""); 44 | return *transport_; 45 | } 46 | 47 | static Ptr make(cetl::pmr::memory_resource& general_mr, 48 | libcyphal::IExecutor& executor, 49 | platform::BlockMemoryResource& media_block_mr, 50 | const Application::IfaceParams& params) 51 | 52 | { 53 | if (params.udp_iface.value().empty()) 54 | { 55 | return nullptr; 56 | } 57 | 58 | auto any_transport_bag = cetl::pmr::InterfaceFactory::make_unique( // 59 | cetl::pmr::polymorphic_allocator{&general_mr}, 60 | Spec{}, 61 | general_mr, 62 | executor, 63 | media_block_mr); 64 | if (!any_transport_bag) 65 | { 66 | return nullptr; 67 | } 68 | auto& bag = static_cast(*any_transport_bag); // NOLINT 69 | 70 | bag.media_collection_.parse(params.udp_iface.value(), params.udp_mtu.value()[0]); 71 | auto maybe_udp_transport = makeTransport({general_mr}, executor, bag.media_collection_.span(), TxQueueCapacity); 72 | if (const auto* failure = cetl::get_if(&maybe_udp_transport)) 73 | { 74 | std::cerr << "❌ Failed to create UDP transport (iface='" 75 | << static_cast(params.udp_iface.value()) << "').\n"; 76 | return nullptr; 77 | } 78 | bag.transport_ = cetl::get>( // 79 | std::move(maybe_udp_transport)); 80 | 81 | std::cout << "UDP Iface : '" << params.udp_iface.value().c_str() << "'\n"; 82 | const std::size_t mtu = bag.transport_->getProtocolParams().mtu_bytes; 83 | std::cout << "Iface MTU : " << mtu << "\n"; 84 | 85 | // Udpard allocates memory for raw bytes block only, so there is no alignment requirement. 86 | constexpr std::size_t block_alignment = 1; 87 | const std::size_t block_size = mtu; 88 | const std::size_t pool_size = bag.media_collection_.count() * TxQueueCapacity * block_size; 89 | bag.media_block_mr_.setup(pool_size, block_size, block_alignment); 90 | 91 | bag.transport_->setTransientErrorHandler(platform::CommonHelpers::Udp::transientErrorReporter); 92 | 93 | return any_transport_bag; 94 | } 95 | 96 | TransportBagUdp(Spec, 97 | cetl::pmr::memory_resource& general_memory, 98 | libcyphal::IExecutor& executor, 99 | platform::BlockMemoryResource& media_block_mr) 100 | : general_mr_{general_memory} 101 | , executor_{executor} 102 | , media_block_mr_{media_block_mr} 103 | , media_collection_{general_memory, executor, media_block_mr} 104 | { 105 | } 106 | 107 | private: 108 | using TransportPtr = libcyphal::UniquePtr; 109 | 110 | static constexpr std::size_t TxQueueCapacity = 16; 111 | 112 | cetl::pmr::memory_resource& general_mr_; 113 | libcyphal::IExecutor& executor_; 114 | platform::BlockMemoryResource& media_block_mr_; 115 | platform::posix::UdpMediaCollection media_collection_; 116 | TransportPtr transport_; 117 | 118 | }; // TransportBagUdp 119 | 120 | #endif // TRANSPORT_BAG_UDP_HPP_INCLUDED 121 | -------------------------------------------------------------------------------- /libudpard_demo/.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: >- 3 | boost-*, 4 | bugprone-*, 5 | cert-*, 6 | clang-analyzer-*, 7 | cppcoreguidelines-*, 8 | google-*, 9 | hicpp-*, 10 | llvm-*, 11 | misc-*, 12 | modernize-*, 13 | performance-*, 14 | portability-*, 15 | readability-*, 16 | -google-readability-todo, 17 | -readability-avoid-const-params-in-decls, 18 | -readability-identifier-length, 19 | -cppcoreguidelines-avoid-magic-numbers, 20 | -bugprone-easily-swappable-parameters, 21 | -llvm-header-guard, 22 | -llvm-include-order, 23 | -cert-dcl03-c, 24 | -hicpp-static-assert, 25 | -misc-static-assert, 26 | -modernize-macro-to-enum, 27 | -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, 28 | CheckOptions: 29 | - key: readability-function-cognitive-complexity.Threshold 30 | value: '99' 31 | - key: readability-magic-numbers.IgnoredIntegerValues 32 | value: '1;2;3;4;5;8;10;16;20;32;50;60;64;100;128;256;500;512;1000' 33 | WarningsAsErrors: '*' 34 | HeaderFilterRegex: '.*' 35 | AnalyzeTemporaryDtors: false 36 | FormatStyle: file 37 | ... 38 | -------------------------------------------------------------------------------- /libudpard_demo/.idea/dictionaries/pavel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | abcdefghijklmnopqrstuvwxyz 5 | awaitables 6 | baremetal 7 | cavl 8 | cfamily 9 | cyphal 10 | deallocation 11 | deduplicator 12 | discardment 13 | dscp 14 | dsdl 15 | dudpard 16 | execve 17 | ffee 18 | gettime 19 | ghcr 20 | iface 21 | ifaces 22 | inaddr 23 | intravehicular 24 | kirienko 25 | libcanard 26 | libgtest 27 | libudpard 28 | loopback 29 | memcpy 30 | memoryview 31 | misoptimizes 32 | mmcu 33 | nnvg 34 | nosonar 35 | opencyphal 36 | pard 37 | perfcounters 38 | prio 39 | profraw 40 | pycyphal 41 | recv 42 | reuseport 43 | sapog 44 | spdx 45 | stringmakers 46 | uavcan 47 | udpard 48 | udpip 49 | unicast 50 | unlisten 51 | unregistration 52 | usec 53 | wconversion 54 | wextra 55 | wtype 56 | xopen 57 | zzzzzz 58 | 59 | 60 | -------------------------------------------------------------------------------- /libudpard_demo/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This software is distributed under the terms of the MIT License. 2 | # Copyright (C) OpenCyphal Development Team 3 | # Copyright Amazon.com Inc. or its affiliates. 4 | # SPDX-License-Identifier: MIT 5 | # Author: Pavel Kirienko 6 | 7 | cmake_minimum_required(VERSION 3.25) 8 | 9 | project(libudpard_demo C) 10 | set(CMAKE_C_STANDARD 11) 11 | set(submodules "${CMAKE_CURRENT_SOURCE_DIR}/../submodules") 12 | set(CMAKE_PREFIX_PATH "${submodules}/nunavut") 13 | 14 | # Set up static analysis. 15 | set(STATIC_ANALYSIS ON CACHE BOOL "enable static analysis") 16 | if (STATIC_ANALYSIS) 17 | # clang-tidy (separate config files per directory) 18 | find_program(clang_tidy NAMES clang-tidy) 19 | if (NOT clang_tidy) 20 | message(WARNING "Could not locate clang-tidy") 21 | set(STATIC_ANALYSIS OFF) 22 | else() 23 | message(STATUS "Using clang-tidy: ${clang_tidy}") 24 | endif () 25 | endif () 26 | 27 | # Forward the revision information to the compiler so that we could expose it at runtime. This is entirely optional. 28 | execute_process( 29 | COMMAND git rev-parse --short=16 HEAD 30 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 31 | OUTPUT_VARIABLE vcs_revision_id 32 | OUTPUT_STRIP_TRAILING_WHITESPACE 33 | ) 34 | message(STATUS "vcs_revision_id: ${vcs_revision_id}") 35 | add_definitions( 36 | -DVERSION_MAJOR=1 37 | -DVERSION_MINOR=0 38 | -DVCS_REVISION_ID=0x${vcs_revision_id}ULL 39 | -DNODE_NAME="org.opencyphal.demos.libudpard" 40 | ) 41 | 42 | # Transpile DSDL into C using Nunavut. This uses this repo's built-in submodules to setup Nunavut. See 43 | # CMAKE_PREFIX_PATH above for how this is resolved to the local submodules. 44 | find_package(Nunavut 3.0 REQUIRED) 45 | 46 | set(LOCAL_PUBLIC_TYPES 47 | uavcan/node/7509.Heartbeat.1.0.dsdl 48 | uavcan/node/port/7510.List.1.0.dsdl 49 | uavcan/node/430.GetInfo.1.0.dsdl 50 | uavcan/node/435.ExecuteCommand.1.1.dsdl 51 | uavcan/register/384.Access.1.0.dsdl 52 | uavcan/register/385.List.1.0.dsdl 53 | uavcan/pnp/8165.NodeIDAllocationData.2.0.dsdl 54 | uavcan/primitive/array/Real32.1.0.dsdl 55 | ) 56 | 57 | add_cyphal_library( 58 | NAME dsdl_uavcan 59 | EXACT_NAME 60 | LANGUAGE c 61 | LANGUAGE_STANDARD c${CMAKE_C_STANDARD} 62 | DSDL_FILES ${LOCAL_PUBLIC_TYPES} 63 | SERIALIZATION_ASSERT assert 64 | EXPORT_MANIFEST 65 | OUT_LIBRARY_TARGET LOCAL_TYPES_C_LIBRARY 66 | ) 67 | 68 | # Define the LibUDPard static library build target. No special options are needed to use the library, it's very simple. 69 | add_library(udpard_demo STATIC ${submodules}/libudpard/libudpard/udpard.c) 70 | target_include_directories(udpard_demo INTERFACE SYSTEM ${submodules}/libudpard/libudpard) 71 | 72 | include(${CMAKE_CURRENT_SOURCE_DIR}/../shared/udp/udp.cmake) 73 | 74 | # Define the demo application build target and link it with the library. 75 | add_executable( 76 | demo 77 | ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c 78 | ${CMAKE_CURRENT_SOURCE_DIR}/src/storage.c 79 | ${CMAKE_CURRENT_SOURCE_DIR}/src/register.c 80 | ) 81 | target_include_directories(demo PRIVATE ${submodules}/cavl) 82 | target_link_libraries(demo PRIVATE ${LOCAL_TYPES_C_LIBRARY} udpard_demo shared_udp) 83 | set_target_properties( 84 | demo 85 | PROPERTIES 86 | COMPILE_FLAGS "-Wall -Wextra -Werror -pedantic -Wdouble-promotion -Wswitch-enum -Wfloat-equal \ 87 | -Wundef -Wconversion -Wtype-limits -Wsign-conversion -Wcast-align -Wmissing-declarations" 88 | C_STANDARD 11 89 | C_EXTENSIONS OFF 90 | ) 91 | if (STATIC_ANALYSIS) 92 | set_target_properties(demo PROPERTIES C_CLANG_TIDY "${clang_tidy}") 93 | endif () 94 | -------------------------------------------------------------------------------- /libudpard_demo/README.md: -------------------------------------------------------------------------------- 1 | # LibUDPard demo application 2 | 3 | This demo application is a usage demonstrator for [LibUDPard](https://github.com/OpenCyphal-Garage/libudpard) --- 4 | a compact Cyphal/UDP implementation for high-integrity systems written in C99. 5 | It implements a simple Cyphal node that showcases the following features: 6 | 7 | - Fixed port-ID and non-fixed port-ID publishers. 8 | - Fixed port-ID and non-fixed port-ID subscribers. 9 | - Fixed port-ID RPC server. 10 | - Plug-and-play node-ID allocation unless it is configured statically. 11 | - Fast Cyphal Register API and non-volatile storage for the persistent registers. 12 | - Support for redundant network interfaces. 13 | 14 | This document will walk you through the process of building, running, and evaluating the demo 15 | on a GNU/Linux-based OS. 16 | It can be easily ported to another platform, such as a baremetal MCU, 17 | by replacing the POSIX socket API and stdio with suitable alternatives; 18 | for details, please consult with `udp.h` and `storage.h`. 19 | 20 | ## Preparation 21 | 22 | Install the [Yakut](https://github.com/OpenCyphal/yakut) CLI tool, 23 | Wireshark with the [Cyphal plugins](https://github.com/OpenCyphal/wireshark_plugins), 24 | and ensure you have CMake and a C11 compiler. 25 | Build the demo: 26 | 27 | ```shell 28 | git clone --recursive https://github.com/OpenCyphal/demos 29 | cd demos/libudpard_demo 30 | mkdir build && cd build 31 | cmake .. && make 32 | ``` 33 | 34 | ## Running 35 | 36 | At the first launch the default parameters will be used. 37 | Upon their modification, the state will be saved on the filesystem in the current working directory 38 | -- you will see a new file appear per parameter (register). 39 | 40 | Per the default settings, the node will use only the local loopback interface. 41 | If it were an embedded system, it could be designed to run a DHCP client to configure the local interface(s) 42 | automatically and then use that configuration. 43 | 44 | Run the node: 45 | 46 | ```shell 47 | ./demo 48 | ``` 49 | 50 | It will print a few informational messages and then go silent. 51 | With the default configuration being missing, the node will be attempting to perform a plug-and-play node-ID allocation 52 | by sending allocation requests forever until an allocation response is received. 53 | You can see this activity --- PnP requests being published --- using Wireshark; 54 | to exclude unnecessary traffic, use the following BPF expression: 55 | 56 | ```bpf 57 | udp and dst net 239.0.0.0 mask 255.0.0.0 and dst port 9382 58 | ``` 59 | 60 | Wireshark capture of a PnP request 61 | 62 | It will keep doing this forever until it got an allocation response from the node-ID allocator. 63 | Note that most high-integrity systems would always assign static node-ID instead of relying on this behavior 64 | to ensure deterministic behaviors at startup. 65 | 66 | To let our application complete the PnP allocation stage, we launch the PnP allocator implemented in Yakut monitor 67 | (this can be done in any working directory): 68 | 69 | ```shell 70 | UAVCAN__UDP__IFACE="127.0.0.1" UAVCAN__NODE__ID=$(yakut accommodate) y mon -P allocation_table.db 71 | ``` 72 | 73 | This will create a new file called `allocation_table.db` containing, well, the node-ID allocation table. 74 | Once the allocation is done (it takes a couple of seconds), the application will announce this by printing a message, 75 | and then the normal operation will be commenced. 76 | The Yakut monitor will display the following picture: 77 | 78 | Yakut monitor output after PnP allocation 79 | 80 | The newly allocated node-ID value will be stored in the `uavcan.node.id` register, 81 | but it will not be committed into the non-volatile storage until the node is commanded to restart. 82 | This is because storage I/O is not compatible with real-time execution, 83 | so the storage is only accessed during startup of the node (to read the values from the non-volatile memory) 84 | and immediately before shutdown (to commit values into the non-volatile memory). 85 | 86 | It is best to keep the Yakut monitor running and execute all subsequent commands in other shell sessions. 87 | 88 | Suppose we want the node to use another network interface aside from the local loopback `127.0.0.1`. 89 | This is done by entering additional local interface addresses in the `uavcan.udp.iface` register separated by space. 90 | You can do this with the help of Yakut as follows (here we are assuming that the allocated node-ID is 65532): 91 | 92 | ```shell 93 | export UAVCAN__UDP__IFACE="127.0.0.1" # Pro tip: move these export statements into a shell file and source it. 94 | export UAVCAN__NODE__ID=$(yakut accommodate) 95 | y r 65532 uavcan.udp.iface "127.0.0.1 192.168.1.200" # Update the local addresses to match your setup. 96 | ``` 97 | 98 | To let the new configuration take effect, the node has to be restarted. 99 | Before restart the register values will be committed into the non-volatile storage; 100 | configuration files will appear in the current working directory of the application. 101 | 102 | ```shell 103 | y cmd 65532 restart 104 | ``` 105 | 106 | The Wireshark capture will show that the node is now sending data via two interfaces concurrently 107 | (or however many you configured). 108 | 109 | Next we will evaluate the application-specific publisher and subscriber. 110 | The application can receive messages of type `uavcan.primitive.array.Real32.1` 111 | and re-publish them with the reversed order of the elements. 112 | The corresponding publisher and subscriber ports are both named `my_data`, 113 | and their port-ID registers are named `uavcan.pub.my_data.id` and `uavcan.sub.my_data.id`, 114 | per standard convention. 115 | As these ports are not configured yet, the node is unable to make use of them. 116 | Configure them manually as follows 117 | (you can also use a YAML file with the `y rb` command for convenience; for more info see Yakut documentation): 118 | 119 | ```shell 120 | y r 65532 uavcan.pub.my_data.id 1001 # You can pick arbitrary values here. 121 | y r 65532 uavcan.sub.my_data.id 1002 122 | ``` 123 | 124 | Then restart the node, and the Yakut monitor will display the newly configured ports in the connectivity matrix: 125 | 126 | Yakut monitor showing the data topics in the connectivity matrix 127 | 128 | To evaluate this part of the application, 129 | subscribe to the topic it is supposed to publish on using the Yakut subscriber tool: 130 | 131 | ```shell 132 | y sub +M 1001:uavcan.primitive.array.real32 133 | ``` 134 | 135 | Use another terminal to publish some data on the topic that the application is subscribed to 136 | (you can use a joystick to generate data dynamically; see the Yakut documentation for more info): 137 | 138 | ```shell 139 | y pub 1002:uavcan.primitive.array.real32 '[50, 60, 70]' 140 | ``` 141 | 142 | The subscriber we launched earlier will show the messages published by our node: 143 | 144 | ```yaml 145 | 1001: 146 | _meta_: {ts_system: 1693821518.340156, ts_monotonic: 1213296.516168, source_node_id: 65532, transfer_id: 183, priority: nominal, dtype: uavcan.primitive.array.Real32.1.0} 147 | value: [70.0, 60.0, 50.0] 148 | ``` 149 | 150 | The current status of the memory allocators can be checked by reading the corresponding diagnostic register 151 | as shown below. 152 | Diagnostic registers are a powerful tool for building advanced introspection interfaces and can be used to expose 153 | and even modify arbitrary internal states of the application over the network. 154 | 155 | ```yaml 156 | y r 65532 sys.info.mem 157 | ``` 158 | 159 | Publishing and subscribing using different remote machines (instead of using the local loopback interface) 160 | is left as an exercise to the reader. 161 | Cyphal/UDP is a masterless peer protocol that does not require manual configuration of the networking infrastructure. 162 | As long as the local interface addresses are set correctly, the Cyphal distributed system will just work out of the box. 163 | Thus, you can simply run the same publisher and subscriber commands on different computers 164 | and see the demo node behave the same way. 165 | 166 | The only difference to keep in mind is that Yakut monitor may fail to display all network traffic unless the computer 167 | it is running on is connected to a SPAN port (mirrored port) of the network switch. 168 | This is because by default, IGMP-compliant switches will not forward multicast packets into ports for which 169 | no members for the corresponding multicast groups are registered. 170 | 171 | To reset the node configuration back to defaults, use this: 172 | 173 | ```shell 174 | y cmd 65532 factory_reset 175 | ``` 176 | 177 | As the application is not allowed to access the storage I/O during runtime, 178 | the factory reset will not take place until the node is restarted. 179 | Once restarted, the configuration files will disappear from the current working directory. 180 | 181 | ## Porting 182 | 183 | Just read the code. Focus your attention on `udp.c` and `storage.c`. 184 | -------------------------------------------------------------------------------- /libudpard_demo/docs/wireshark-pnp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCyphal-Garage/demos/87741d8242bcb27b39e22115559a4b91e92ffe06/libudpard_demo/docs/wireshark-pnp.png -------------------------------------------------------------------------------- /libudpard_demo/docs/yakut-monitor-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCyphal-Garage/demos/87741d8242bcb27b39e22115559a4b91e92ffe06/libudpard_demo/docs/yakut-monitor-data.png -------------------------------------------------------------------------------- /libudpard_demo/docs/yakut-monitor-pnp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCyphal-Garage/demos/87741d8242bcb27b39e22115559a4b91e92ffe06/libudpard_demo/docs/yakut-monitor-pnp.png -------------------------------------------------------------------------------- /libudpard_demo/src/crc64we.h: -------------------------------------------------------------------------------- 1 | /// ____ ______ __ __ 2 | /// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / 3 | /// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / 4 | /// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / 5 | /// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ 6 | /// /_/ /____/_/ 7 | /// 8 | /// A simple implementation of the CRC-64/WE hash function. For details, please refer to 9 | /// http://reveng.sourceforge.net/crc-catalogue/17plus.htm#crc.cat-bits.64 10 | /// 11 | /// This software is distributed under the terms of the MIT License. 12 | /// Copyright (C) OpenCyphal Development Team 13 | /// Copyright Amazon.com Inc. or its affiliates. 14 | /// SPDX-License-Identifier: MIT 15 | /// Author: Pavel Kirienko 16 | 17 | #pragma once 18 | 19 | #include 20 | #include 21 | 22 | static inline uint64_t crc64we(const size_t size, const void* const data) 23 | { 24 | static const uint64_t Poly = 0x42F0E1EBA9EA3693ULL; 25 | static const uint64_t Mask = 1ULL << 63U; 26 | static const uint64_t InputShift = 56U; 27 | static const uint64_t OctetWidth = 8U; 28 | uint64_t hash = UINT64_MAX; 29 | for (size_t i = 0; i < size; i++) 30 | { 31 | hash ^= (uint64_t) (*(i + (const unsigned char*) data)) << InputShift; 32 | for (uint_fast8_t j = 0; j < OctetWidth; j++) 33 | { 34 | hash = ((hash & Mask) != 0) ? ((hash << 1U) ^ Poly) : (hash << 1U); 35 | } 36 | } 37 | return hash ^ UINT64_MAX; 38 | } 39 | 40 | static inline uint64_t crc64weString(const char* const str) 41 | { 42 | return crc64we(strlen(str), str); 43 | } 44 | -------------------------------------------------------------------------------- /libudpard_demo/src/memory_block.h: -------------------------------------------------------------------------------- 1 | /// ____ ______ __ __ 2 | /// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / 3 | /// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / 4 | /// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / 5 | /// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ 6 | /// /_/ /____/_/ 7 | /// 8 | /// A header-only implementation of a fairly standard O(1) free list block allocator, aka block pool allocator. 9 | /// It is not required to use LibUDPard, as the library can be used with an ordinary malloc()/free() or 10 | /// perhaps O1Heap (https://github.com/pavel-kirienko/o1heap) if the application requires a more sophisticated 11 | /// memory management strategy. 12 | /// 13 | /// This software is distributed under the terms of the MIT License. 14 | /// Copyright (C) OpenCyphal Development Team 15 | /// Copyright Amazon.com Inc. or its affiliates. 16 | /// SPDX-License-Identifier: MIT 17 | /// Author: Pavel Kirienko 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | /// This can be replaced with the standard malloc()/free(), if available. 25 | /// This macro is a crude substitute for the missing metaprogramming facilities in C. 26 | #define MEMORY_BLOCK_ALLOCATOR_DEFINE(_name, _block_size_bytes, _block_count) \ 27 | _Alignas(max_align_t) static uint_least8_t _name##_pool[(_block_size_bytes) * (_block_count)]; \ 28 | struct MemoryBlockAllocator _name = memoryBlockInit(sizeof(_name##_pool), &_name##_pool[0], (_block_size_bytes)) 29 | 30 | /// The user shall not attempt to change any of the fields manually. 31 | struct MemoryBlockAllocator 32 | { 33 | // Constant pool parameters. 34 | size_t block_count; 35 | size_t block_size_bytes; 36 | 37 | // Read-only diagnostic values. 38 | size_t used_blocks; ///< Blocks in use at the moment. 39 | size_t used_blocks_peak; ///< Maximum number of blocks used at any point in time. 40 | uint64_t request_count; ///< Total number of allocation requests. 41 | uint64_t oom_count; ///< Total number of out-of-memory errors. 42 | 43 | // Private fields. 44 | void* head; 45 | }; 46 | 47 | /// Constructs a memory block allocator bound to the specified memory pool. 48 | /// The block count will be deduced from the pool size and block size; both may be adjusted to ensure alignment. 49 | /// If the pool or block size are not properly aligned, some memory may need to be wasted to enforce alignment. 50 | static struct MemoryBlockAllocator memoryBlockInit(const size_t pool_size_bytes, 51 | void* const pool, 52 | const size_t block_size_bytes) 53 | { 54 | // Enforce alignment and padding of the input arguments. We may waste some space as a result. 55 | const size_t bs = (block_size_bytes + sizeof(max_align_t) - 1U) & ~(sizeof(max_align_t) - 1U); 56 | size_t sz_bytes = pool_size_bytes; 57 | uint_least8_t* ptr = (uint_least8_t*) pool; 58 | while ((((uintptr_t) ptr) % sizeof(max_align_t)) != 0) 59 | { 60 | ptr++; 61 | if (sz_bytes > 0) 62 | { 63 | sz_bytes--; 64 | } 65 | } 66 | const size_t block_count = sz_bytes / bs; 67 | for (size_t i = 0; i < block_count; i++) 68 | { 69 | *(void**) (void*) (ptr + (i * bs)) = ((i + 1) < block_count) ? ((void*) (ptr + ((i + 1) * bs))) : NULL; 70 | } 71 | const struct MemoryBlockAllocator out = {.block_count = block_count, .block_size_bytes = bs, .head = ptr}; 72 | return out; 73 | } 74 | 75 | static void* memoryBlockAllocate(void* const user_reference, const size_t size) 76 | { 77 | void* out = NULL; 78 | struct MemoryBlockAllocator* const self = (struct MemoryBlockAllocator*) user_reference; 79 | assert(self != NULL); 80 | self->request_count++; 81 | if ((size > 0) && (size <= self->block_size_bytes)) 82 | { 83 | out = self->head; 84 | if (self->head != NULL) 85 | { 86 | self->head = *(void**) self->head; 87 | self->used_blocks++; 88 | self->used_blocks_peak = 89 | (self->used_blocks > self->used_blocks_peak) ? self->used_blocks : self->used_blocks_peak; 90 | } 91 | } 92 | if (out == NULL) 93 | { 94 | self->oom_count++; 95 | } 96 | return out; 97 | } 98 | 99 | static void memoryBlockDeallocate(void* const user_reference, const size_t size, void* const pointer) 100 | { 101 | struct MemoryBlockAllocator* const self = (struct MemoryBlockAllocator*) user_reference; 102 | assert((self != NULL) && (size <= self->block_size_bytes)); 103 | (void) size; 104 | if (pointer != NULL) 105 | { 106 | *(void**) pointer = self->head; 107 | self->head = pointer; 108 | assert(self->used_blocks > 0); 109 | self->used_blocks--; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /libudpard_demo/src/register.c: -------------------------------------------------------------------------------- 1 | /// This software is distributed under the terms of the MIT License. 2 | /// Copyright (C) OpenCyphal Development Team 3 | /// Copyright Amazon.com Inc. or its affiliates. 4 | /// SPDX-License-Identifier: MIT 5 | /// Author: Pavel Kirienko 6 | 7 | #include "register.h" 8 | #include "crc64we.h" 9 | #include 10 | 11 | #define ASSIGN_SAME_TYPE(dst, src, type) \ 12 | do \ 13 | { \ 14 | if (uavcan_register_Value_1_0_is_##type##_(dst) && uavcan_register_Value_1_0_is_##type##_(src)) \ 15 | { \ 16 | for (size_t i = 0; i < nunavutChooseMin((dst)->type.value.count, (src)->type.value.count); ++i) \ 17 | { \ 18 | (dst)->type.value.elements[i] = (src)->type.value.elements[i]; \ 19 | } \ 20 | return true; \ 21 | } \ 22 | } while (0) 23 | 24 | static int_fast8_t treeSearchHash(void* const user_reference, const struct Cavl* const node) 25 | { 26 | const uint64_t lhs = *(const uint64_t*) user_reference; 27 | const uint64_t rhs = ((const struct Register*) node)->name_hash; 28 | return (int_fast8_t) ((lhs < rhs) ? -1 : (lhs > rhs) ? 1 : 0); 29 | } 30 | static int_fast8_t treeSearchReg(void* const user_reference, const struct Cavl* const node) 31 | { 32 | return treeSearchHash(&((struct Register*) user_reference)->name_hash, node); 33 | } 34 | static Cavl* treeFactory(void* const user_reference) 35 | { 36 | return (Cavl*) user_reference; 37 | } 38 | 39 | void registerInit(struct Register* const self, 40 | struct Register** const root, 41 | const char** const null_terminated_name_fragments) 42 | { 43 | assert((self != NULL) && (root != NULL) && (null_terminated_name_fragments != NULL)); 44 | (void) memset(self, 0, sizeof(*self)); 45 | { 46 | const char** nf = null_terminated_name_fragments; 47 | char* wp = &self->name[0]; 48 | size_t remaining = sizeof(self->name); 49 | while (*nf != NULL) 50 | { 51 | if ((nf != null_terminated_name_fragments) && (remaining > 0)) 52 | { 53 | *wp++ = '.'; 54 | remaining--; 55 | } 56 | const size_t frag_len = strlen(*nf); 57 | const size_t copy_size = (frag_len < remaining) ? frag_len : remaining; 58 | (void) memcpy(wp, *nf, copy_size); 59 | wp += copy_size; 60 | remaining -= copy_size; 61 | nf++; 62 | } 63 | self->name[uavcan_register_Name_1_0_name_ARRAY_CAPACITY_] = '\0'; 64 | } 65 | self->name_hash = crc64weString(self->name); 66 | self->getter = NULL; 67 | // Insert the register into the tree. Remove the old one if it exists. 68 | cavlRemove((Cavl**) root, cavlSearch((Cavl**) root, self, &treeSearchReg, NULL)); 69 | const Cavl* const res = cavlSearch((Cavl**) root, self, &treeSearchReg, &treeFactory); 70 | assert(res == &self->base); 71 | (void) res; 72 | } 73 | 74 | bool registerAssign(uavcan_register_Value_1_0* const dst, const uavcan_register_Value_1_0* const src) 75 | { 76 | if (uavcan_register_Value_1_0_is_empty_(dst)) 77 | { 78 | *dst = *src; 79 | return true; 80 | } 81 | if ((uavcan_register_Value_1_0_is_string_(dst) && uavcan_register_Value_1_0_is_string_(src)) || 82 | (uavcan_register_Value_1_0_is_unstructured_(dst) && uavcan_register_Value_1_0_is_unstructured_(src))) 83 | { 84 | *dst = *src; 85 | return true; 86 | } 87 | if (uavcan_register_Value_1_0_is_bit_(dst) && uavcan_register_Value_1_0_is_bit_(src)) 88 | { 89 | nunavutCopyBits(dst->bit.value.bitpacked, 90 | 0, 91 | nunavutChooseMin(dst->bit.value.count, src->bit.value.count), 92 | src->bit.value.bitpacked, 93 | 0); 94 | return true; 95 | } 96 | ASSIGN_SAME_TYPE(dst, src, integer64); 97 | ASSIGN_SAME_TYPE(dst, src, integer32); 98 | ASSIGN_SAME_TYPE(dst, src, integer16); 99 | ASSIGN_SAME_TYPE(dst, src, integer8); 100 | ASSIGN_SAME_TYPE(dst, src, natural64); 101 | ASSIGN_SAME_TYPE(dst, src, natural32); 102 | ASSIGN_SAME_TYPE(dst, src, natural16); 103 | ASSIGN_SAME_TYPE(dst, src, natural8); 104 | ASSIGN_SAME_TYPE(dst, src, real64); 105 | ASSIGN_SAME_TYPE(dst, src, real32); 106 | ASSIGN_SAME_TYPE(dst, src, real16); 107 | return false; 108 | } 109 | 110 | // NOLINTNEXTLINE(*-no-recursion) 111 | void* registerTraverse(struct Register* const root, 112 | void* (*const fun)(struct Register*, void*), 113 | void* const user_reference) 114 | { 115 | void* out = NULL; 116 | if (root != NULL) 117 | { 118 | out = registerTraverse((struct Register*) root->base.lr[0], fun, user_reference); 119 | if (out == NULL) 120 | { 121 | out = fun(root, user_reference); 122 | } 123 | if (out == NULL) 124 | { 125 | out = registerTraverse((struct Register*) root->base.lr[1], fun, user_reference); 126 | } 127 | } 128 | return out; 129 | } 130 | 131 | struct Register* registerFindByName(struct Register* const root, const char* const name) 132 | { 133 | uint64_t name_hash = crc64weString(name); 134 | return (struct Register*) cavlSearch((Cavl**) &root, &name_hash, &treeSearchHash, NULL); 135 | } 136 | 137 | static void* indexTraverseFun(struct Register* const self, void* const user_reference) 138 | { 139 | assert(user_reference != NULL); 140 | return ((*(size_t*) user_reference)-- == 0) ? self : NULL; 141 | } 142 | struct Register* registerFindByIndex(struct Register* const root, const size_t index) 143 | { 144 | size_t i = index; 145 | return (struct Register*) registerTraverse(root, &indexTraverseFun, &i); 146 | } 147 | -------------------------------------------------------------------------------- /libudpard_demo/src/register.h: -------------------------------------------------------------------------------- 1 | /// ____ ______ __ __ 2 | /// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / 3 | /// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / 4 | /// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / 5 | /// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ 6 | /// /_/ /____/_/ 7 | /// 8 | /// In Cyphal, registers are named values that can be read and possibly written over the network. They are used 9 | /// extensively to configure the application and to expose its states to the network. This module provides one 10 | /// possible implementation of the Cyphal register API. Since the implementation of the register API does not 11 | /// affect wire compatibility, the Cyphal Specification does not mandate any particular approach; as such, 12 | /// other applications may approach the same problem differently. 13 | /// 14 | /// This design is based on an AVL tree indexed by the CRC-64/WE hash of the register name. The tree is used 15 | /// to quickly find the register by name in logarithmic time. The tree is also traversed in the index order to 16 | /// implement the index-based access. 17 | /// 18 | /// The intended usage is to initialize all registers at boot-up and then never modify the tree again. 19 | /// Persistent registers (those that are stored in the non-volatile storage, typically these are the node configuration 20 | /// parameters) should be read from the storage once during boot-up, in order to relieve the application from accessing 21 | /// the potentially slow and time-unpredictable blocking storage API during normal operation. If changes need to be 22 | /// made to the non-volatile storage, they should be made immediately before the application terminates (before reboot), 23 | /// when the application is no longer confined by the stringent real-time requirements and can invoke the blocking 24 | /// storage API safely. 25 | /// 26 | /// This software is distributed under the terms of the MIT License. 27 | /// Copyright (C) OpenCyphal Development Team 28 | /// Copyright Amazon.com Inc. or its affiliates. 29 | /// SPDX-License-Identifier: MIT 30 | /// Author: Pavel Kirienko 31 | 32 | #pragma once 33 | 34 | #include 35 | #include 36 | 37 | /// Represents a local node register. 38 | /// Cyphal registers are named values described in the uavcan.register.Access DSDL definition. 39 | /// This type can also be used as a supertype to implement advanced capabilities. 40 | struct Register 41 | { 42 | Cavl base; ///< Do not modify. 43 | 44 | /// The name is always null-terminated. The name hash is used for faster lookup to avoid string comparisons. 45 | char name[uavcan_register_Name_1_0_name_ARRAY_CAPACITY_ + 1]; 46 | uint64_t name_hash; 47 | 48 | /// The metadata fields are mostly useful when serving remote access requests. 49 | bool persistent; ///< The value is stored in non-volatile memory. The application is responsible for that. 50 | bool remote_mutable; ///< The value can be changed over the network. 51 | 52 | /// The type of the value shall not change after initialization (see the registry API specification). 53 | /// If accessor functions are used, the value of this field is ignored/undefined. 54 | uavcan_register_Value_1_0 value; 55 | /// If getter is non-NULL, the value will be obtained through this callback instead of the value field. 56 | uavcan_register_Value_1_0 (*getter)(struct Register*); 57 | 58 | /// An arbitrary user-defined pointer. This is mostly useful with the callbacks. 59 | void* user_reference; 60 | }; 61 | 62 | /// Inserts the register into the tree. The caller must initialize the value/getter fields after this call. 63 | /// Behavior undefined if the self or root or name fragment pointers are NULL. 64 | /// The name fragment sequence is terminated by a NULL pointer; the fragments will be joined via "."; e.g., 65 | /// ("uavcan", "node", "id", NULL) --> "uavcan.node.id". 66 | /// If such register already exists, it will be replaced. 67 | /// 68 | /// The register is initialized as non-persistent and immutable; this can be changed afterward. 69 | /// 70 | /// Addition of a new register invalidates the indexes; hence, in the interest of preserving the indexes, 71 | /// the application should init all registers at once during startup and then never modify the tree. 72 | void registerInit(struct Register* const self, 73 | struct Register** const root, 74 | const char** const null_terminated_name_fragments); 75 | 76 | /// Copy one value to the other if their types and dimensionality are the same or an automatic conversion is possible. 77 | /// If the destination is empty, it is simply replaced with the source (assignment always succeeds). 78 | /// Assignment always fails if the source is empty, unless the destination is also empty. 79 | /// The return value is true if the assignment has been performed, false if it is not possible; 80 | /// in the latter case the destination is not modified. 81 | /// 82 | /// This function is useful when accepting register write requests from the network. 83 | bool registerAssign(uavcan_register_Value_1_0* const dst, const uavcan_register_Value_1_0* const src); 84 | 85 | /// Traverse all registers in their index order, invoke the functor on each. 86 | /// The user reference will be passed to the functor as-is. 87 | /// Traversal will stop either when all registers are traversed or when the functor returns non-NULL. 88 | /// Returns the pointer returned by the functor or NULL if all registers were traversed. 89 | /// This can be used for loading the persistent registers from the non-volatile storage at boot-up 90 | /// and for storing them back before reboot, among other things. 91 | void* registerTraverse(struct Register* const root, 92 | void* (*const fun)(struct Register*, void*), 93 | void* const user_reference); 94 | 95 | /// These search functions are needed to implement the Cyphal register API. 96 | /// Returns NULL if not found or the arguments are invalid. 97 | struct Register* registerFindByName(struct Register* const root, const char* const name); 98 | struct Register* registerFindByIndex(struct Register* const root, const size_t index); 99 | -------------------------------------------------------------------------------- /libudpard_demo/src/storage.c: -------------------------------------------------------------------------------- 1 | /// This software is distributed under the terms of the MIT License. 2 | /// Copyright (C) 2021 OpenCyphal 3 | /// Author: Pavel Kirienko 4 | 5 | #include "storage.h" 6 | #include "crc64we.h" 7 | #include 8 | #include 9 | #include 10 | 11 | /// We could use key names directly, but they may be long, which leads to unnecessary storage memory consumption; 12 | /// additionally, certain storage implementations may not be able to store arbitrary-length keys. 13 | /// Therefore, we use a hash of the key. It makes key listing impossible but it is not required by the application. 14 | /// 15 | /// The hash length can be adjusted as necessary to trade-off collision probability vs. space requirement. 16 | /// If the underlying hash function is CRC64 and the radix is 62, the maximum sensible hash size is 11 digits. 17 | /// A 7-digit hash with radix 62 is representable in a ceil(log2(62**7)) = 43-bit integer. 18 | /// A 6-digit hash with radix 62 is representable in a ceil(log2(62**6)) = 36-bit integer. 19 | /// The collision probability for a 6-digit base-62 hash with 200 keys is: 20 | /// 21 | /// >>> n=200 22 | /// >>> d=Decimal(62**6) 23 | /// >>> 1- ((d-1)/d) ** ((n*(n-1))//2) 24 | /// 3.5e-7 25 | /// >>> _*100 26 | /// 0.000035 27 | #define KEY_HASH_LENGTH 7U 28 | struct KeyHash 29 | { 30 | char hash[KEY_HASH_LENGTH + 1]; // Digits plus the null terminator. 31 | }; 32 | 33 | #define KEY_EXTENSION ".cfg" 34 | struct KeyPath 35 | { 36 | char path[KEY_HASH_LENGTH + sizeof(KEY_EXTENSION) + 1]; 37 | }; 38 | 39 | static struct KeyHash computeKeyHash(const char* const key) 40 | { 41 | static const char Alphabet[] = "0123456789" 42 | "abcdefghijklmnopqrstuvwxyz" 43 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 44 | static const uint64_t Radix = sizeof(Alphabet) - 1U; 45 | uint64_t value = crc64weString(key); 46 | size_t index = 0; 47 | struct KeyHash out = {.hash = {0}}; // The hash contains the digits in reverse order. 48 | do 49 | { 50 | out.hash[index] = Alphabet[value % Radix]; 51 | value /= Radix; 52 | index++; 53 | } while ((value > 0) && (index < KEY_HASH_LENGTH)); 54 | return out; 55 | } 56 | 57 | static struct KeyPath computeKeyPath(const char* const key) 58 | { 59 | struct KeyPath out = {.path = {0}}; 60 | (void) memcpy(&out.path[0], &computeKeyHash(key).hash[0], KEY_HASH_LENGTH); 61 | (void) memcpy(&out.path[KEY_HASH_LENGTH], KEY_EXTENSION, strlen(KEY_EXTENSION)); 62 | return out; 63 | } 64 | 65 | static FILE* keyOpen(const char* const key, const bool write) 66 | { 67 | return fopen(&computeKeyPath(key).path[0], write ? "wb" : "rb"); 68 | } 69 | 70 | bool storageGet(const char* const key, size_t* const inout_size, void* const data) 71 | { 72 | bool result = false; 73 | if ((key != NULL) && (inout_size != NULL) && (data != NULL)) 74 | { 75 | FILE* const fp = keyOpen(key, false); 76 | if (fp != NULL) 77 | { 78 | *inout_size = fread(data, 1U, *inout_size, fp); 79 | result = (ferror(fp) == 0); 80 | (void) fclose(fp); 81 | } 82 | } 83 | return result; 84 | } 85 | 86 | bool storagePut(const char* const key, const size_t size, const void* const data) 87 | { 88 | bool result = false; 89 | if ((key != NULL) && (data != NULL)) 90 | { 91 | FILE* const fp = keyOpen(key, true); 92 | if (fp != NULL) 93 | { 94 | result = (fwrite(data, 1U, size, fp) == size) && (ferror(fp) == 0); 95 | (void) fclose(fp); 96 | } 97 | } 98 | return result; 99 | } 100 | 101 | bool storageDrop(const char* const key) 102 | { 103 | return (key != NULL) && (unlink(&computeKeyPath(key).path[0]) == 0); 104 | } 105 | -------------------------------------------------------------------------------- /libudpard_demo/src/storage.h: -------------------------------------------------------------------------------- 1 | /// ____ ______ __ __ 2 | /// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / 3 | /// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / 4 | /// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / 5 | /// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ 6 | /// /_/ /____/_/ 7 | /// 8 | /// This module implements non-volatile key-value storage for this application. On an embedded system this may be 9 | /// backed by a trivial key-value list stored in a memory chip, or perhaps a compact fault-tolerant file system, 10 | /// like LittleFS. High-integrity embedded systems may benefit from not accessing the storage memory during 11 | /// normal operation at all; the recommended approach is to read the configuration from the storage memory 12 | /// once during the boot-up and then keep it in RAM; if new configuration needs to be stored, it is to be done 13 | /// immediately before the reboot. 14 | /// 15 | /// This software is distributed under the terms of the MIT License. 16 | /// Copyright (C) OpenCyphal Development Team 17 | /// Copyright Amazon.com Inc. or its affiliates. 18 | /// SPDX-License-Identifier: MIT 19 | /// Author: Pavel Kirienko 20 | 21 | #pragma once 22 | 23 | #include 24 | #include 25 | 26 | /// Returns true on success, false if there is no such key, I/O error, or bad parameters. 27 | /// At entry, inout_size contains the size of the output buffer; upon return it contains the number of bytes read. 28 | /// 29 | /// The function is blocking and the execution time is not rigidly bounded, so it should only be invoked 30 | /// during the boot-up or before the reboot. 31 | bool storageGet(const char* const key, size_t* const inout_size, void* const data); 32 | 33 | /// Existing key will be overwritten. If there is no such key, it will be created. 34 | /// This function should normally be only called before the reboot when configuration changes need to be committed 35 | /// to the non-volatile storage. Optionally it may be invoked during the boot-up, if required by the application 36 | /// logic, but never during normal operation. 37 | /// Returns true on success, false on I/O error or bad parameters. 38 | bool storagePut(const char* const key, const size_t size, const void* const data); 39 | 40 | /// Removes the key from the storage. Does nothing if the key does not exist. 41 | /// This is useful when the configuration needs to be reset to the default values, or during version migration. 42 | /// Returns true on success, false on I/O error or bad parameters. 43 | /// 44 | /// The function is blocking and the execution time is not rigidly bounded, so it should only be invoked 45 | /// during the boot-up or before the reboot. 46 | bool storageDrop(const char* const key); 47 | -------------------------------------------------------------------------------- /shared/register/register.c: -------------------------------------------------------------------------------- 1 | /// This software is distributed under the terms of the MIT License. 2 | /// Copyright (C) 2021 OpenCyphal 3 | /// Author: Pavel Kirienko 4 | 5 | #include "register.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define PASTE3_IMPL(x, y, z) x##y##z 15 | #define PASTE3(x, y, z) PASTE3_IMPL(x, y, z) 16 | 17 | static const char RegistryDirName[] = "registry"; 18 | 19 | static inline FILE* registerOpen(const char* const register_name, const bool write) 20 | { 21 | // An actual implementation on an embedded system may need to perform atomic file transactions via rename(). 22 | char file_path[uavcan_register_Name_1_0_name_ARRAY_CAPACITY_ + sizeof(RegistryDirName) + 2] = {0}; 23 | (void) snprintf(&file_path[0], sizeof(file_path), "%s/%s", RegistryDirName, register_name); 24 | if (write) 25 | { 26 | (void) mkdir(RegistryDirName, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); 27 | } 28 | return fopen(&file_path[0], write ? "wb" : "rb"); 29 | } 30 | 31 | void registerRead(const char* const register_name, uavcan_register_Value_1_0* const inout_value) 32 | { 33 | assert(inout_value != NULL); 34 | bool init_required = !uavcan_register_Value_1_0_is_empty_(inout_value); 35 | FILE* const fp = registerOpen(®ister_name[0], false); 36 | if (fp != NULL) 37 | { 38 | uint8_t serialized[uavcan_register_Value_1_0_EXTENT_BYTES_]; 39 | size_t sr_size = fread(&serialized[0], 1U, uavcan_register_Value_1_0_EXTENT_BYTES_, fp); 40 | (void) fclose(fp); 41 | uavcan_register_Value_1_0 out = {0}; 42 | const int8_t err = uavcan_register_Value_1_0_deserialize_(&out, serialized, &sr_size); 43 | if (err >= 0) 44 | { 45 | init_required = !registerAssign(inout_value, &out); 46 | } 47 | } 48 | if (init_required) 49 | { 50 | printf("Init register: %s\n", register_name); 51 | registerWrite(register_name, inout_value); 52 | } 53 | } 54 | 55 | void registerWrite(const char* const register_name, const uavcan_register_Value_1_0* const value) 56 | { 57 | uint8_t serialized[uavcan_register_Value_1_0_EXTENT_BYTES_]; 58 | size_t sr_size = uavcan_register_Value_1_0_EXTENT_BYTES_; 59 | const int8_t err = uavcan_register_Value_1_0_serialize_(value, serialized, &sr_size); 60 | if (err >= 0) 61 | { 62 | FILE* const fp = registerOpen(®ister_name[0], true); 63 | if (fp != NULL) 64 | { 65 | (void) fwrite(&serialized[0], 1U, sr_size, fp); 66 | (void) fclose(fp); 67 | } 68 | } 69 | } 70 | 71 | uavcan_register_Name_1_0 registerGetNameByIndex(const uint16_t index) 72 | { 73 | uavcan_register_Name_1_0 out = {0}; 74 | uavcan_register_Name_1_0_initialize_(&out); 75 | DIR* const dp = opendir(RegistryDirName); 76 | if (dp != NULL) 77 | { 78 | // The service definition requires that the ordering is consistent between calls. 79 | // We assume here that there will be no new registers added while the listing operation is in progress. 80 | // If this is not the case, you will need to implement additional logic to uphold the ordering consistency 81 | // guarantee, such as sorting registers by creation time or adding extra metadata. 82 | struct dirent* ep = readdir(dp); 83 | uint16_t ii = 0; 84 | while (ep != NULL) 85 | { 86 | if (ep->d_type == DT_REG) 87 | { 88 | if (ii >= index) 89 | { 90 | break; 91 | } 92 | ++ii; 93 | } 94 | ep = readdir(dp); 95 | } 96 | if (ep != NULL) 97 | { 98 | out.name.count = nunavutChooseMin(strlen(ep->d_name), uavcan_register_Name_1_0_name_ARRAY_CAPACITY_); 99 | memcpy(out.name.elements, ep->d_name, out.name.count); 100 | } 101 | (void) closedir(dp); 102 | } 103 | return out; 104 | } 105 | 106 | bool registerAssign(uavcan_register_Value_1_0* const dst, const uavcan_register_Value_1_0* const src) 107 | { 108 | if (uavcan_register_Value_1_0_is_empty_(dst)) 109 | { 110 | *dst = *src; 111 | return true; 112 | } 113 | if ((uavcan_register_Value_1_0_is_string_(dst) && uavcan_register_Value_1_0_is_string_(src)) || 114 | (uavcan_register_Value_1_0_is_unstructured_(dst) && uavcan_register_Value_1_0_is_unstructured_(src))) 115 | { 116 | *dst = *src; 117 | return true; 118 | } 119 | if (uavcan_register_Value_1_0_is_bit_(dst) && uavcan_register_Value_1_0_is_bit_(src)) 120 | { 121 | nunavutCopyBits(dst->bit.value.bitpacked, 122 | 0, 123 | nunavutChooseMin(dst->bit.value.count, src->bit.value.count), 124 | src->bit.value.bitpacked, 125 | 0); 126 | return true; 127 | } 128 | // This is a violation of MISRA/AUTOSAR but it is believed to be less error-prone than manually copy-pasted code. 129 | #define REGISTER_CASE_SAME_TYPE(TYPE) \ 130 | if (PASTE3(uavcan_register_Value_1_0_is_, TYPE, _)(dst) && PASTE3(uavcan_register_Value_1_0_is_, TYPE, _)(src)) \ 131 | { \ 132 | for (size_t i = 0; i < nunavutChooseMin(dst->TYPE.value.count, src->TYPE.value.count); ++i) \ 133 | { \ 134 | dst->TYPE.value.elements[i] = src->TYPE.value.elements[i]; \ 135 | } \ 136 | return true; \ 137 | } 138 | REGISTER_CASE_SAME_TYPE(integer64) 139 | REGISTER_CASE_SAME_TYPE(integer32) 140 | REGISTER_CASE_SAME_TYPE(integer16) 141 | REGISTER_CASE_SAME_TYPE(integer8) 142 | REGISTER_CASE_SAME_TYPE(natural64) 143 | REGISTER_CASE_SAME_TYPE(natural32) 144 | REGISTER_CASE_SAME_TYPE(natural16) 145 | REGISTER_CASE_SAME_TYPE(natural8) 146 | REGISTER_CASE_SAME_TYPE(real64) 147 | REGISTER_CASE_SAME_TYPE(real32) 148 | REGISTER_CASE_SAME_TYPE(real16) 149 | return false; 150 | } 151 | 152 | void registerDoFactoryReset(void) 153 | { 154 | DIR* const dp = opendir(RegistryDirName); 155 | if (dp != NULL) 156 | { 157 | struct dirent* ep = readdir(dp); 158 | while (ep != NULL) 159 | { 160 | if (ep->d_type == DT_REG) 161 | { 162 | char file_path[uavcan_register_Name_1_0_name_ARRAY_CAPACITY_ + sizeof(RegistryDirName) + 2] = {0}; 163 | (void) snprintf(&file_path[0], sizeof(file_path), "%s/%s", RegistryDirName, ep->d_name); 164 | (void) unlink(&file_path[0]); 165 | } 166 | ep = readdir(dp); 167 | } 168 | (void) closedir(dp); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /shared/register/register.cmake: -------------------------------------------------------------------------------- 1 | # This software is distributed under the terms of the MIT License. 2 | # Copyright (C) OpenCyphal Development Team 3 | # Copyright Amazon.com Inc. or its affiliates. 4 | # SPDX-License-Identifier: MIT 5 | # Author: Sergei Shirokov 6 | 7 | cmake_minimum_required(VERSION 3.20) 8 | 9 | # Define the demo application build target and link it with the library. 10 | add_library( 11 | shared_register 12 | ${CMAKE_CURRENT_LIST_DIR}/register.c 13 | ) 14 | target_include_directories(shared_register PUBLIC ${CMAKE_CURRENT_LIST_DIR}) 15 | -------------------------------------------------------------------------------- /shared/register/register.h: -------------------------------------------------------------------------------- 1 | /// ____ ______ __ __ 2 | /// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / 3 | /// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / 4 | /// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / 5 | /// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ 6 | /// /_/ /____/_/ 7 | /// 8 | /// Registers are named values that keep various configuration parameters of the local Cyphal node (application). 9 | /// Some of these parameters are used by the business logic of the application (e.g., PID gains, perfcounters); 10 | /// others are used by the Cyphal stack (e.g., port-IDs, node-ID, transport configuration, introspection, and so on). 11 | /// Registers of the latter category are all named with the same prefix "uavcan.", and their names and semantics 12 | /// are regulated by the Cyphal Specification to ensure consistency across the ecosystem. 13 | /// 14 | /// The Specification doesn't define how the registers are to be stored since this part does not affect network 15 | /// interoperability. In this demo we use a very simple and portable approach where each register is stored as 16 | /// a separate file in the local filesystem; the name of the file matches the name of the register, and the register 17 | /// values are serialized in the DSDL format (i.e., same format that is used for network exchange). 18 | /// Deeply embedded systems may either use the same approach with the help of some compact fault-tolerant filesystem 19 | /// (such as, for example, LittleFS: https://github.com/littlefs-project/littlefs), or they can resort to a low-level 20 | /// specialized approach using on-chip EEPROM or similar (like PX4, Sapog, etc). 21 | /// 22 | /// This software is distributed under the terms of the MIT License. 23 | /// Copyright (C) 2021 OpenCyphal 24 | /// Author: Pavel Kirienko 25 | 26 | #pragma once 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | // NOTE: this implementation currently does not differentiate between mutable/immutable registers and does not support 34 | // volatile registers. It is trivial to extend though. 35 | 36 | /// Reads the specified register from the persistent storage into `inout_value`. 37 | /// If the register does not exist or it cannot be automatically converted to the type of the provided argument, 38 | /// the value will be stored in the persistent storage using @ref registerWrite(), overriding existing value. 39 | /// The default will not be initialized if the argument is empty. 40 | void registerRead(const char* const register_name, uavcan_register_Value_1_0* const inout_value); 41 | 42 | /// Store the given register value into the persistent storage. 43 | void registerWrite(const char* const register_name, const uavcan_register_Value_1_0* const value); 44 | 45 | /// This function is mostly intended for implementing the standard RPC-service uavcan.register.List. 46 | /// It returns the name of the register at the specified index (where the ordering is undefined but guaranteed 47 | /// to be short-term stable), or empty name if the index is out of bounds. 48 | uavcan_register_Name_1_0 registerGetNameByIndex(const uint16_t index); 49 | 50 | /// Copy one value to the other if their types and dimensionality are the same or automatic conversion is possible. 51 | /// If the destination is empty, it is simply replaced with the source (assignment always succeeds). 52 | /// The return value is true if the assignment has been performed, false if it is not possible 53 | /// (in the latter case the destination is NOT modified). 54 | bool registerAssign(uavcan_register_Value_1_0* const dst, const uavcan_register_Value_1_0* const src); 55 | 56 | /// Erase all registers such that the defaults are used at the next launch. 57 | void registerDoFactoryReset(void); 58 | -------------------------------------------------------------------------------- /shared/socketcan/socketcan.cmake: -------------------------------------------------------------------------------- 1 | # This software is distributed under the terms of the MIT License. 2 | # Copyright (C) OpenCyphal Development Team 3 | # Copyright Amazon.com Inc. or its affiliates. 4 | # SPDX-License-Identifier: MIT 5 | # Author: Sergei Shirokov 6 | 7 | cmake_minimum_required(VERSION 3.20) 8 | 9 | # Define the demo application build target and link it with the library. 10 | add_library( 11 | shared_socketcan 12 | ${CMAKE_CURRENT_LIST_DIR}/socketcan.c 13 | ) 14 | target_link_libraries(shared_socketcan PUBLIC canard) 15 | target_include_directories(shared_socketcan PUBLIC ${CMAKE_CURRENT_LIST_DIR}) 16 | -------------------------------------------------------------------------------- /shared/socketcan/socketcan.h: -------------------------------------------------------------------------------- 1 | /// ____ ______ __ __ 2 | /// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / 3 | /// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / 4 | /// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / 5 | /// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ 6 | /// /_/ /____/_/ 7 | /// 8 | /// This is a basic adapter library that bridges Libcanard with SocketCAN. 9 | /// Read the API documentation for usage information. 10 | /// 11 | /// To integrate the library into your application, just copy-paste the c/h files into your project tree. 12 | /// 13 | /// -------------------------------------------------------------------------------------------------------------------- 14 | /// Changelog 15 | /// 16 | /// v3.0 - Update for compatibility with Libcanard v3. 17 | /// 18 | /// v2.0 - Added loop-back functionality. 19 | /// API change in socketcanPop(): loopback flag added. 20 | /// - Changed to kernel-based time-stamping for received frames for improved accuracy. 21 | /// API change in socketcanPop(): time stamp clock source is now CLOCK_REALTIME, vs CLOCK_TAI before. 22 | /// 23 | /// v1.0 - Initial release 24 | /// -------------------------------------------------------------------------------------------------------------------- 25 | /// 26 | /// This software is distributed under the terms of the MIT License. 27 | /// Copyright (c) 2020 OpenCyphal 28 | /// Author: Pavel Kirienko 29 | 30 | #ifndef SOCKETCAN_H_INCLUDED 31 | #define SOCKETCAN_H_INCLUDED 32 | 33 | #include "canard.h" 34 | #include 35 | #include 36 | #include 37 | 38 | #ifdef __cplusplus 39 | extern "C" { 40 | #endif 41 | 42 | /// File descriptor alias. 43 | typedef int SocketCANFD; 44 | 45 | /// Initialize a new non-blocking (sic!) SocketCAN socket and return its handle on success. 46 | /// On failure, a negated errno is returned. 47 | /// To discard the socket just call close() on it; no additional de-initialization activities are required. 48 | /// The argument can_mtu specifies CAN MTU size: 8 - for classic; >8 (64 normally) - for CAN FD frames. 49 | /// In the future, this MTU size may be extended to support CAN XL. 50 | SocketCANFD socketcanOpen(const char* const iface_name, const size_t can_mtu); 51 | 52 | /// Enqueue a new extended CAN data frame for transmission. 53 | /// Block until the frame is enqueued or until the timeout is expired. 54 | /// Zero timeout makes the operation non-blocking. 55 | /// Returns 1 on success, 0 on timeout, negated errno on error. 56 | int16_t socketcanPush(const SocketCANFD fd, 57 | const struct CanardFrame* const frame, 58 | const CanardMicrosecond timeout_usec); 59 | 60 | /// Fetch a new extended CAN data frame from the RX queue. 61 | /// If the received frame is not an extended-ID data frame, it will be dropped and the function will return early. 62 | /// The payload pointer of the returned frame will point to the payload_buffer. It can be a stack-allocated array. 63 | /// The payload_buffer_size shall be large enough (64 bytes is enough for CAN FD), otherwise an error is returned. 64 | /// The received frame timestamp will be set to CLOCK_REALTIME by the kernel, sampled near the moment of its arrival. 65 | /// The loopback flag pointer is used to both indicate and control behavior when a looped-back message is received. 66 | /// If the flag pointer is NULL, loopback frames are silently dropped; if not NULL, they are accepted and indicated 67 | /// using this flag. 68 | /// The function will block until a frame is received or until the timeout is expired. It may return early. 69 | /// Zero timeout makes the operation non-blocking. 70 | /// Returns 1 on success, 0 on timeout, negated errno on error. 71 | int16_t socketcanPop(const SocketCANFD fd, 72 | struct CanardFrame* const out_frame, 73 | CanardMicrosecond* const out_timestamp_usec, 74 | const size_t payload_buffer_size, 75 | void* const payload_buffer, 76 | const CanardMicrosecond timeout_usec, 77 | bool* const loopback); 78 | 79 | /// Apply the specified acceptance filter configuration. 80 | /// Note that it is only possible to accept extended-format data frames. 81 | /// The default configuration is to accept everything. 82 | /// Returns 0 on success, negated errno on error. 83 | int16_t socketcanFilter(const SocketCANFD fd, const size_t num_configs, const struct CanardFilter* const configs); 84 | 85 | #ifdef __cplusplus 86 | } 87 | #endif 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /shared/udp/udp.cmake: -------------------------------------------------------------------------------- 1 | # This software is distributed under the terms of the MIT License. 2 | # Copyright (C) OpenCyphal Development Team 3 | # Copyright Amazon.com Inc. or its affiliates. 4 | # SPDX-License-Identifier: MIT 5 | # Author: Sergei Shirokov 6 | 7 | cmake_minimum_required(VERSION 3.20) 8 | 9 | # Define the demo application build target and link it with the library. 10 | add_library( 11 | shared_udp 12 | ${CMAKE_CURRENT_LIST_DIR}/udp.c 13 | ) 14 | target_include_directories(shared_udp PUBLIC ${CMAKE_CURRENT_LIST_DIR}) 15 | -------------------------------------------------------------------------------- /shared/udp/udp.h: -------------------------------------------------------------------------------- 1 | /// ____ ______ __ __ 2 | /// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / 3 | /// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / 4 | /// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / 5 | /// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ 6 | /// /_/ /____/_/ 7 | /// 8 | /// This module implements the platform-specific implementation of the UDP transport. On a conventional POSIX system 9 | /// this would be a thin wrapper around the standard Berkeley sockets API. On a bare-metal system this would be 10 | /// a thin wrapper around the platform-specific network stack, such as LwIP, or a custom solution. 11 | /// 12 | /// Having the interface extracted like this helps better illustrate the surface of the networking API required 13 | /// by LibUDPard, which is minimal. This also helps with porting to new platforms. 14 | /// 15 | /// All addresses and values used in this API are in the host-native byte order. 16 | /// For example, 127.0.0.1 is represented as 0x7F000001 always. 17 | /// 18 | /// This software is distributed under the terms of the MIT License. 19 | /// Copyright (C) OpenCyphal Development Team 20 | /// Copyright Amazon.com Inc. or its affiliates. 21 | /// SPDX-License-Identifier: MIT 22 | /// Author: Pavel Kirienko 23 | 24 | #pragma once 25 | 26 | #include 27 | #include 28 | #include 29 | 30 | #ifdef __cplusplus 31 | extern "C" { 32 | #endif 33 | 34 | /// These definitions are highly platform-specific. 35 | /// Note that LibUDPard does not require the same socket to be usable for both transmission and reception. 36 | typedef struct 37 | { 38 | int fd; 39 | } UDPTxHandle; 40 | typedef struct 41 | { 42 | int fd; 43 | } UDPRxHandle; 44 | 45 | /// Initialize a TX socket for use with LibUDPard. 46 | /// The local iface address is used to specify the egress interface for multicast traffic. 47 | /// Per LibUDPard design, there is one TX socket per redundant interface, so the application needs to invoke 48 | /// this function once per interface. 49 | /// On error returns a negative error code. 50 | int16_t udpTxInit(UDPTxHandle* const self, const uint32_t local_iface_address); 51 | 52 | /// Send a datagram to the specified endpoint without blocking using the specified IP DSCP field value. 53 | /// A real-time embedded system should normally accept a transmission deadline here for the networking stack. 54 | /// Returns 1 on success, 0 if the socket is not ready for sending, or a negative error code. 55 | int16_t udpTxSend(UDPTxHandle* const self, 56 | const uint32_t remote_address, 57 | const uint16_t remote_port, 58 | const uint8_t dscp, 59 | const size_t payload_size, 60 | const void* const payload); 61 | 62 | /// No effect if the argument is invalid. 63 | /// This function is guaranteed to invalidate the handle. 64 | void udpTxClose(UDPTxHandle* const self); 65 | 66 | /// Initialize an RX socket for use with LibUDPard, for subscription to subjects or for RPC traffic. 67 | /// The socket will be bound to the specified multicast group and port. 68 | /// Most socket APIs, in particular the Berkeley sockets, require the local iface address to be known, 69 | /// because it is used to decide which egress port to send IGMP membership reports over. 70 | /// On error returns a negative error code. 71 | int16_t udpRxInit(UDPRxHandle* const self, 72 | const uint32_t local_iface_address, 73 | const uint32_t multicast_group, 74 | const uint16_t remote_port); 75 | 76 | /// Read one datagram from the socket without blocking. 77 | /// The size of the destination buffer is specified in inout_payload_size; it is updated to the actual size of the 78 | /// received datagram upon return. 79 | /// Returns 1 on success, 0 if the socket is not ready for reading, or a negative error code. 80 | int16_t udpRxReceive(UDPRxHandle* const self, size_t* const inout_payload_size, void* const out_payload); 81 | 82 | /// No effect if the argument is invalid. 83 | /// This function is guaranteed to invalidate the handle. 84 | void udpRxClose(UDPRxHandle* const self); 85 | 86 | /// Auxiliary types for use with the I/O multiplexing function. 87 | /// The "ready" flag is updated to indicate whether the handle is ready for I/O. 88 | /// The "user_*" fields can be used for user-defined purposes. 89 | typedef struct 90 | { 91 | UDPTxHandle* handle; 92 | bool ready; 93 | void* user_reference; 94 | } UDPTxAwaitable; 95 | typedef struct 96 | { 97 | UDPRxHandle* handle; 98 | bool ready; 99 | void* user_reference; 100 | } UDPRxAwaitable; 101 | 102 | /// Suspend execution until the expiration of the timeout (in microseconds) or until any of the specified handles 103 | /// become ready for reading (the RX group) or writing (the TX group). 104 | /// The function may return earlier than the timeout even if no handles are ready. 105 | /// On error returns a negative error code. 106 | int16_t udpWait(const uint64_t timeout_usec, 107 | const size_t tx_count, 108 | UDPTxAwaitable* const tx, 109 | const size_t rx_count, 110 | UDPRxAwaitable* const rx); 111 | 112 | /// Convert an interface address from string to binary representation; e.g., "127.0.0.1" --> 0x7F000001. 113 | /// Returns zero if the address is not recognized. 114 | uint32_t udpParseIfaceAddress(const char* const address); 115 | 116 | #ifdef __cplusplus 117 | } 118 | #endif 119 | -------------------------------------------------------------------------------- /udral_servo/.idea/dictionaries/pavel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | afdx 5 | allocatee 6 | antipattern 7 | appveyor 8 | argparse 9 | asctime 10 | asyncio 11 | atexit 12 | aton 13 | autoconfiguration 14 | autodoc 15 | baremetal 16 | baudrate 17 | bgcolor 18 | blin 19 | bools 20 | bufferless 21 | byref 22 | bysource 23 | candump 24 | canfd 25 | canid 26 | caplog 27 | capturable 28 | ccitt 29 | cmsg 30 | coloredlogs 31 | computron 32 | comspec 33 | conftest 34 | constrainedness 35 | constructible 36 | contnode 37 | creationflags 38 | ctrunc 39 | cyber 40 | cyphal 41 | datagrams 42 | deadbeef 43 | deallocated 44 | debian 45 | decaxta 46 | deduplicator 47 | deduplicators 48 | demultiplexer 49 | demultiplexing 50 | demux 51 | dereplicated 52 | deserializing 53 | dhcp 54 | diehard 55 | disbalance 56 | dlen 57 | docname 58 | doctree 59 | doesn 60 | dronecode 61 | dscp 62 | dsdl 63 | dsonar 64 | dtype 65 | elif 66 | emptor 67 | endfor 68 | endmacro 69 | ethertype 70 | facto 71 | ffee 72 | ffff 73 | fgsfds 74 | findalldevs 75 | fontname 76 | frombuffer 77 | fyodor 78 | geez 79 | gendsdl 80 | genericity 81 | getenv 82 | getpeername 83 | getsockopt 84 | gibibyte 85 | gibibytes 86 | graphviz 87 | hacky 88 | hardbass 89 | hdlc 90 | hitl 91 | hostmask 92 | hwgrep 93 | hwmaj 94 | hwmin 95 | iana 96 | icmp 97 | iface 98 | ifaces 99 | ifidx 100 | inaddr 101 | inet 102 | intersphinx 103 | intravehicular 104 | ipproto 105 | iscsi 106 | isfinite 107 | kibibyte 108 | kirienko 109 | koopman 110 | levelname 111 | libcanard 112 | libpcap 113 | libuavcan 114 | linkcode 115 | lnid 116 | lsmod 117 | mcfloatface 118 | mebibytes 119 | memcpy 120 | memoryview 121 | mismaintenance 122 | modifyitems 123 | modprobe 124 | mult 125 | multiframe 126 | multithreaded 127 | mypy 128 | mypypath 129 | ncat 130 | ndarray 131 | ndim 132 | ndis 133 | netcat 134 | netfilter 135 | netstat 136 | nfrag 137 | nihil 138 | nmap 139 | nnvg 140 | noqa 141 | norecursedirs 142 | nosignatures 143 | npcap 144 | ntdll 145 | octothorp 146 | onboard 147 | opencyphal 148 | packbits 149 | pathlib 150 | pcap 151 | pcapy 152 | perfcounters 153 | pfft 154 | pizdec 155 | pizdets 156 | pkgutil 157 | popen 158 | powershell 159 | prio 160 | prog 161 | protip 162 | pydev 163 | pydsdl 164 | pydsdlgen 165 | pygments 166 | pylint 167 | pyserial 168 | pytest 169 | pythonasynciodebug 170 | pythoncan 171 | pythonpath 172 | pythonunbuffered 173 | pyuavcan 174 | pyyaml 175 | quantizer 176 | qube 177 | qwertyui 178 | qwertyuiop 179 | raii 180 | rankdir 181 | rawsource 182 | readlines 183 | readthedocs 184 | reasm 185 | reasms 186 | reassembler 187 | reassemblers 188 | rechunk 189 | recvfrom 190 | recvmsg 191 | refdoc 192 | refid 193 | refragment 194 | refragmented 195 | reftarget 196 | reftype 197 | reftypes 198 | relbar 199 | relbars 200 | representer 201 | representers 202 | repurposeability 203 | reuseport 204 | roundtrip 205 | rtfd 206 | rtps 207 | ruamel 208 | runas 209 | searchbox 210 | sendable 211 | sendmsg 212 | sert 213 | serv 214 | setcap 215 | setpoint 216 | setsockopt 217 | sheremet 218 | signedness 219 | sitecustomize 220 | sitl 221 | slcan 222 | sockaddr 223 | socketcan 224 | socketcanfd 225 | sphinxarg 226 | sssss 227 | ssssssss 228 | stdint 229 | strictification 230 | subcommand 231 | subcommands 232 | sublayers 233 | submoduling 234 | subnets 235 | subparser 236 | suka 237 | supernum 238 | supremum 239 | swmaj 240 | swmin 241 | synth 242 | systeminfo 243 | telega 244 | tempfile 245 | templatedir 246 | testpaths 247 | thumby 248 | timestamping 249 | tobytes 250 | tocfile 251 | toctree 252 | todos 253 | tradeoff 254 | typecheck 255 | uart 256 | uavcan 257 | uber 258 | udpros 259 | udral 260 | ulong 261 | uname 262 | unconfigured 263 | undisable 264 | undoc 265 | unhashable 266 | unicast 267 | unseparate 268 | unstropped 269 | upvote 270 | usec 271 | usercustomize 272 | vcan 273 | versioning 274 | veyor 275 | virtualization 276 | voldemort 277 | vpaun 278 | vssc 279 | weakref 280 | winpcap 281 | wireshark 282 | worl 283 | wpcap 284 | xbee 285 | xfer 286 | zubax 287 | 288 | 289 | -------------------------------------------------------------------------------- /udral_servo/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This software is distributed under the terms of the MIT License. 2 | # Copyright (C) 2021 OpenCyphal 3 | # Author: Pavel Kirienko 4 | 5 | cmake_minimum_required(VERSION 3.25) 6 | project(udral_servo_demo C) 7 | 8 | set(submodules "${CMAKE_CURRENT_SOURCE_DIR}/../submodules") 9 | list(APPEND CMAKE_PREFIX_PATH "${submodules}/nunavut") 10 | 11 | set(CMAKE_C_STANDARD 11) 12 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Werror -pedantic -fstrict-aliasing") 13 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wdouble-promotion -Wswitch-enum -Wfloat-equal -Wundef") 14 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wconversion -Wtype-limits") 15 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wsign-conversion -Wcast-align -Wmissing-declarations") 16 | 17 | # Forward the revision information to the compiler so that we could expose it at runtime. This is entirely optional. 18 | execute_process( 19 | COMMAND git rev-parse --short=16 HEAD 20 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 21 | OUTPUT_VARIABLE vcs_revision_id 22 | OUTPUT_STRIP_TRAILING_WHITESPACE 23 | ) 24 | message(STATUS "vcs_revision_id: ${vcs_revision_id}") 25 | add_definitions( 26 | -DVERSION_MAJOR=1 27 | -DVERSION_MINOR=0 28 | -DVCS_REVISION_ID=0x${vcs_revision_id}ULL 29 | -DNODE_NAME="org.opencyphal.demos.udral.servo" 30 | ) 31 | 32 | # Transpile DSDL into C using Nunavut. This uses this repo's built-in submodules to setup Nunavut. See 33 | # CMAKE_PREFIX_PATH above for how this is resolved to the local submodules. 34 | find_package(Nunavut 3.0 REQUIRED) 35 | 36 | set(LOCAL_PUBLIC_REG_TYPES 37 | reg/udral/service/common/Readiness.0.1.dsdl 38 | reg/udral/service/actuator/common/_.0.1.dsdl 39 | reg/udral/service/actuator/common/Feedback.0.1.dsdl 40 | reg/udral/service/actuator/common/Status.0.1.dsdl 41 | reg/udral/physics/dynamics/translation/LinearTs.0.1.dsdl 42 | reg/udral/physics/electricity/PowerTs.0.1.dsdl 43 | uavcan/node/430.GetInfo.1.0.dsdl 44 | uavcan/node/435.ExecuteCommand.1.1.dsdl 45 | uavcan/node/7509.Heartbeat.1.0.dsdl 46 | uavcan/node/port/7510.List.0.1.dsdl 47 | uavcan/pnp/8165.NodeIDAllocationData.2.0.dsdl 48 | uavcan/register/384.Access.1.0.dsdl 49 | uavcan/register/385.List.1.0.dsdl 50 | ) 51 | 52 | add_cyphal_library( 53 | NAME dsdl_uavcan 54 | EXACT_NAME 55 | LANGUAGE c 56 | LANGUAGE_STANDARD c${CMAKE_C_STANDARD} 57 | DSDL_FILES ${LOCAL_PUBLIC_REG_TYPES} 58 | DSDL_NAMESPACES 59 | ${NUNAVUT_SUBMODULES_DIR}/public_regulated_data_types/reg 60 | SERIALIZATION_ASSERT assert 61 | EXPORT_MANIFEST 62 | OUT_LIBRARY_TARGET LOCAL_TYPES_C_LIBRARY 63 | ) 64 | 65 | # Build libcanard. 66 | add_library(canard STATIC ${submodules}/libcanard/libcanard/canard.c) 67 | include_directories(SYSTEM ${submodules}/libcanard/libcanard) 68 | 69 | # Build o1heap -- a hard real-time deterministic memory allocator for embedded systems. 70 | add_library(o1heap STATIC ${submodules}/o1heap/o1heap/o1heap.c) 71 | include_directories(SYSTEM ${submodules}/o1heap/o1heap/) 72 | 73 | include(${CMAKE_CURRENT_SOURCE_DIR}/../shared/register/register.cmake) 74 | target_link_libraries(shared_register 75 | PRIVATE ${LOCAL_TYPES_C_LIBRARY} 76 | ) 77 | 78 | include(${CMAKE_CURRENT_SOURCE_DIR}/../shared/socketcan/socketcan.cmake) 79 | 80 | # Build the application. 81 | add_executable(udral_servo_demo 82 | src/main.c 83 | ) 84 | target_link_libraries(udral_servo_demo 85 | ${LOCAL_TYPES_C_LIBRARY} 86 | canard 87 | o1heap 88 | shared_register 89 | shared_socketcan 90 | ) 91 | -------------------------------------------------------------------------------- /udral_servo/README.md: -------------------------------------------------------------------------------- 1 | # UDRAL servo demo 2 | 3 | ## Purpose 4 | 5 | This demo implements the full [UDRAL](https://github.com/OpenCyphal/public_regulated_data_types/) 6 | servo network service in a highly portable C application that can be trivially 7 | adapted to run in a baremetal environment. 8 | Unless ported, the demo is intended for evaluation on GNU/Linux. 9 | 10 | This demo supports only Cyphal/CAN at the moment, but it can be extended to support Cyphal/UDP or Cyphal/serial. 11 | 12 | The servo network service is defined for two kinds of actuators: translational and rotary. 13 | The only difference is that one uses `reg.udral.physics.dynamics.translation.Linear`, 14 | and the other uses `reg.udral.physics.dynamics.rotation.Planar`. 15 | The types can be replaced if necessary. 16 | 17 | UDRAL comes with a hard requirement that a node shall be equipped with non-volatile memory for keeping the 18 | node registers (that is, configuration parameters). 19 | It is not possible to construct a compliant implementation without non-volatile memory. 20 | 21 | 22 | ## Preparation 23 | 24 | You will need GNU/Linux, CMake, a C11 compiler, [Yakut](https://github.com/OpenCyphal/yakut), 25 | and [SocketCAN utils](https://github.com/linux-can/can-utils). 26 | 27 | Build the demo as follows: 28 | 29 | ```bash 30 | git clone --recursive https://github.com/OpenCyphal/demos 31 | cd demos/udral_servo 32 | mkdir build && cd build 33 | cmake .. && make 34 | ``` 35 | 36 | 37 | ## Running 38 | 39 | Set up a virtual CAN bus `vcan0`: 40 | 41 | ```bash 42 | modprobe can 43 | modprobe can_raw 44 | modprobe vcan 45 | ip link add dev vcan0 type vcan 46 | ip link set vcan0 mtu 72 # Enable CAN FD by configuring the MTU of 64+8 47 | ip link set up vcan0 48 | ``` 49 | 50 | Launch the node 51 | (it is built to emulate an embedded system, so it does not accept any arguments or environment variables): 52 | 53 | ```bash 54 | ./udral_servo_demo 55 | ``` 56 | 57 | It may print a few informational messages and then go silent. 58 | 59 | Fire up the CAN dump utility from SocketCAN utils and see what's happening on the bus. 60 | You should see the PnP node-ID allocation requests being sent by our node irregularly: 61 | 62 | ```bash 63 | $ candump -decaxta vcan0 64 | (1616445708.288978) vcan0 TX B - 197FE510 [20] FF FF C6 69 73 51 FF 4A EC 29 CD BA AB F2 FB E3 46 7C 00 E9 65 | (1616445711.289044) vcan0 TX B - 197FE510 [20] FF FF C6 69 73 51 FF 4A EC 29 CD BA AB F2 FB E3 46 7C 00 EA 66 | # and so on... 67 | ``` 68 | 69 | It will keep doing this forever until it got an allocation response from the node-ID allocator. 70 | 71 | A practical system would always assign static node-ID instead of relying on this behavior to ensure 72 | deterministic behaviors at startup. 73 | This, however, cannot be done until we have a node-ID allocated so that we are able to configure the node via Cyphal. 74 | Therefore, we launch a PnP node-ID allocator available in Yakut (PX4 also implements one): 75 | 76 | ```bash 77 | export UAVCAN__CAN__IFACE="socketcan:vcan0" 78 | export UAVCAN__NODE__ID=127 # This node-ID is for Yakut. 79 | y mon --plug-and-play ~/allocation_table.db 80 | ``` 81 | 82 | This command will run the monitor together with the allocator. 83 | You will see our node get itself a node-ID allocated, 84 | then roughly the following picture should appear on the monitor: 85 | 86 | yakut monitor 87 | 88 | That means that our node is running, 89 | but it is unable to perform any servo-related activities because the respective subjects remain unconfigured. 90 | So let's configure them (do not stop the monitor though, otherwise you won't know what's happening on the bus), 91 | assuming that the node got allocated the node-ID of 125. 92 | First, it helps to know what registers are available at all: 93 | 94 | ```bash 95 | $ export UAVCAN__CAN__IFACE="socketcan:vcan0" 96 | $ export UAVCAN__NODE__ID=126 # This node-ID is for Yakut. 97 | $ y rl 125 98 | [reg.udral.service.actuator.servo, uavcan.can.mtu, uavcan.node.description, uavcan.node.id, uavcan.node.unique_id, uavcan.pub.servo.dynamics.id, uavcan.pub.servo.dynamics.type, uavcan.pub.servo.feedback.id, uavcan.pub.servo.feedback.type, uavcan.pub.servo.power.id, uavcan.pub.servo.power.type, uavcan.pub.servo.status.id, uavcan.pub.servo.status.type, uavcan.sub.servo.readiness.id, uavcan.sub.servo.readiness.type, uavcan.sub.servo.setpoint.id, uavcan.sub.servo.setpoint.type, udral.pnp.cookie] 99 | $ y rl 125, | y rb # You can also read all registers like this 100 | # (output not shown) 101 | ``` 102 | 103 | Configure the subject-IDs (you can also make a YAML file with params and apply it using `y rb`): 104 | 105 | ```bash 106 | y r 125 uavcan.sub.servo.readiness.id 10 107 | y r 125 uavcan.sub.servo.setpoint.id 50 108 | y r 125 uavcan.pub.servo.dynamics.id 100 109 | y r 125 uavcan.pub.servo.feedback.id 101 110 | y r 125 uavcan.pub.servo.power.id 102 111 | y r 125 uavcan.pub.servo.status.id 103 112 | ``` 113 | 114 | The node is configured now, but we need to restart it before the configuration parameter changes take effect: 115 | 116 | ```bash 117 | y cmd 125 restart -e 118 | ``` 119 | 120 | You should see candump start printing a lot more frames (approx. 150 per second). 121 | The demo should still print `DISARMED` in the terminal. 122 | Let's arm it and publish some setpoint (specifying the types is optional): 123 | 124 | ```bash 125 | y pub --period=0.5 --count=30 \ 126 | 10:reg.udral.service.common.readiness 3 \ 127 | 50:reg.udral.physics.dynamics.translation.linear 'kinematics: {position: -3.14}' 128 | ``` 129 | 130 | You will see the message that is printed on the terminal change from `DISARMED` 131 | to the current setpoint values. 132 | The monitor should show you something close to this: 133 | 134 | yakut monitor 135 | 136 | Shortly after the publisher is stopped the servo will automatically disarm itself, as dictated by the UDRAL standard. 137 | 138 | You can listen for the dynamics subject published by the node as follows: 139 | 140 | ```bash 141 | y sub 100:reg.udral.physics.dynamics.translation.LinearTs 142 | ``` 143 | 144 | You can erase the configuration and go back to factory defaults as follows: 145 | 146 | ```bash 147 | y cmd 125 factory_reset 148 | ``` 149 | 150 | If you have a joystick or a MIDI controller, 151 | you can control the servo interactively using `yakut publish` as shown in this video: 152 | 153 | [![using joystick](https://img.youtube.com/vi/wTuWtrrI1m0/maxresdefault.jpg)](https://www.youtube.com/watch?v=wTuWtrrI1m0) 154 | 155 | The corresponding command is (adjust the axes/buttons as necessary): 156 | 157 | ```bash 158 | y pub --period=0.1 \ 159 | 10:reg.udral.service.common.readiness '!$ "T(1,3) * 3"' \ 160 | 50:reg.udral.physics.dynamics.translation.linear 'kinematics: {velocity: !$ "A(1,4)*10"}' 161 | ``` 162 | 163 | 164 | ## Porting 165 | 166 | Just read the code. 167 | 168 | The files `socketcan.[ch]` were taken from . 169 | You may (or may not) find something relevant for your target platform there, too. 170 | -------------------------------------------------------------------------------- /udral_servo/docs/monitor-initial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCyphal-Garage/demos/87741d8242bcb27b39e22115559a4b91e92ffe06/udral_servo/docs/monitor-initial.png -------------------------------------------------------------------------------- /udral_servo/docs/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCyphal-Garage/demos/87741d8242bcb27b39e22115559a4b91e92ffe06/udral_servo/docs/monitor.png --------------------------------------------------------------------------------