Skip to content

Commit 961171b

Browse files
Add LibUDPard demo (#16)
The code is a little sloppy compared to the library itself but it should be adequate for the demo. I moved the platform-specific parts -- the UDP stack and the storage I/O -- into separate modules to make the API surface clearly visible. This also simplifies porting if one wants to run this demo on an MCU. I avoided building strong abstractions on top of LibUDPard because that may obscure the API of the library and attract attention of the reader to the features of the demo instead of those of the library itself. The flipside is that `main.c` has to be fairly large. The Cavl library was added by copying the header file instead of adding the submodule because the entire library is just a single file alone so the submodule approach seemed like an overkill. We can change this if necessary, though. See a related question here: OpenCyphal/libudpard#11
1 parent 598be7a commit 961171b

22 files changed

+3322
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ cmake-build*/
3939
.compiled/
4040
.transpiled/
4141

42+
# Node register files
43+
*.cfg
44+
4245
# IDE and tools
4346
.gdbinit
4447
**/.idea/*

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@
77
[submodule "submodules/o1heap"]
88
path = submodules/o1heap
99
url = https://github.com/pavel-kirienko/o1heap
10+
[submodule "submodules/libudpard"]
11+
path = submodules/libudpard
12+
url = https://github.com/OpenCyphal/libudpard

libudpard_demo/.clang-tidy

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
...

libudpard_demo/.idea/dictionaries/pavel.xml

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libudpard_demo/CMakeLists.txt

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# This software is distributed under the terms of the MIT License.
2+
# Copyright (C) OpenCyphal Development Team <opencyphal.org>
3+
# Copyright Amazon.com Inc. or its affiliates.
4+
# SPDX-License-Identifier: MIT
5+
# Author: Pavel Kirienko <[email protected]>
6+
7+
cmake_minimum_required(VERSION 3.20)
8+
9+
project(libudpard_demo C)
10+
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
11+
set(submodules "${CMAKE_SOURCE_DIR}/../submodules")
12+
13+
# Set up static analysis.
14+
set(STATIC_ANALYSIS OFF CACHE BOOL "enable static analysis")
15+
if (STATIC_ANALYSIS)
16+
# clang-tidy (separate config files per directory)
17+
find_program(clang_tidy NAMES clang-tidy)
18+
if (NOT clang_tidy)
19+
message(FATAL_ERROR "Could not locate clang-tidy")
20+
endif ()
21+
message(STATUS "Using clang-tidy: ${clang_tidy}")
22+
endif ()
23+
24+
# Forward the revision information to the compiler so that we could expose it at runtime. This is entirely optional.
25+
execute_process(
26+
COMMAND git rev-parse --short=16 HEAD
27+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
28+
OUTPUT_VARIABLE vcs_revision_id
29+
OUTPUT_STRIP_TRAILING_WHITESPACE
30+
)
31+
message(STATUS "vcs_revision_id: ${vcs_revision_id}")
32+
add_definitions(
33+
-DVERSION_MAJOR=1
34+
-DVERSION_MINOR=0
35+
-DVCS_REVISION_ID=0x${vcs_revision_id}ULL
36+
-DNODE_NAME="org.opencyphal.demos.libudpard"
37+
)
38+
39+
# Transpile DSDL into C using Nunavut. Install Nunavut as follows: pip install nunavut.
40+
# Alternatively, you can invoke the transpiler manually or use https://nunaweb.opencyphal.org.
41+
find_package(nnvg REQUIRED)
42+
create_dsdl_target( # Generate the support library for generated C headers, which is "nunavut.h".
43+
"nunavut_support"
44+
c
45+
${CMAKE_BINARY_DIR}/transpiled
46+
""
47+
OFF
48+
little
49+
"only"
50+
)
51+
set(dsdl_root_namespace_dirs # List all DSDL root namespaces to transpile here.
52+
${submodules}/public_regulated_data_types/uavcan
53+
${submodules}/public_regulated_data_types/reg
54+
)
55+
foreach (ns_dir ${dsdl_root_namespace_dirs})
56+
get_filename_component(ns ${ns_dir} NAME)
57+
message(STATUS "DSDL namespace ${ns} at ${ns_dir}")
58+
create_dsdl_target(
59+
"dsdl_${ns}" # CMake target name
60+
c # Target language to transpile into
61+
${CMAKE_BINARY_DIR}/transpiled # Destination directory (add it to the includes)
62+
${ns_dir} # Source directory
63+
OFF # Disable variable array capacity override
64+
little # Endianness of the target platform (alternatives: "big", "any")
65+
"never" # Support files are generated once in the nunavut_support target (above)
66+
${dsdl_root_namespace_dirs} # Look-up DSDL namespaces
67+
)
68+
add_dependencies("dsdl_${ns}" nunavut_support)
69+
endforeach ()
70+
include_directories(SYSTEM ${CMAKE_BINARY_DIR}/transpiled) # Make the transpiled headers available for inclusion.
71+
add_definitions(-DNUNAVUT_ASSERT=assert)
72+
73+
# Define the LibUDPard static library build target. No special options are needed to use the library, it's very simple.
74+
add_library(udpard_demo STATIC ${submodules}/libudpard/libudpard/udpard.c)
75+
target_include_directories(udpard_demo INTERFACE SYSTEM ${submodules}/libudpard/libudpard)
76+
77+
# Define the demo application build target and link it with the library.
78+
add_executable(
79+
demo
80+
${CMAKE_SOURCE_DIR}/src/main.c
81+
${CMAKE_SOURCE_DIR}/src/storage.c
82+
${CMAKE_SOURCE_DIR}/src/register.c
83+
${CMAKE_SOURCE_DIR}/src/udp.c
84+
)
85+
target_include_directories(demo PRIVATE ${submodules}/cavl)
86+
target_link_libraries(demo PRIVATE udpard_demo)
87+
add_dependencies(demo dsdl_uavcan dsdl_reg)
88+
set_target_properties(
89+
demo
90+
PROPERTIES
91+
COMPILE_FLAGS "-Wall -Wextra -Werror -pedantic -Wdouble-promotion -Wswitch-enum -Wfloat-equal \
92+
-Wundef -Wconversion -Wtype-limits -Wsign-conversion -Wcast-align -Wmissing-declarations"
93+
C_STANDARD 11
94+
C_EXTENSIONS OFF
95+
)
96+
if (STATIC_ANALYSIS)
97+
set_target_properties(demo PROPERTIES C_CLANG_TIDY "${clang_tidy}")
98+
endif ()

libudpard_demo/README.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
<img src="docs/wireshark-pnp.png" alt="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+
<img src="docs/yakut-monitor-pnp.png" alt="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+
<img src="docs/yakut-monitor-data.png" alt="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`.

0 commit comments

Comments
 (0)