Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LibUDPard demo #16

Merged
merged 36 commits into from Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e784baa
Start libudpard demo
pavel-kirienko Aug 16, 2023
ac8ace9
Add dictionary
pavel-kirienko Aug 16, 2023
b432a01
Fix source path
pavel-kirienko Aug 16, 2023
a1103a6
Add memory allocator
pavel-kirienko Aug 18, 2023
524933d
Builds
pavel-kirienko Aug 23, 2023
acd7695
Runs quite well
pavel-kirienko Aug 23, 2023
39cc78b
UID
pavel-kirienko Aug 23, 2023
ad0337d
Add registers
pavel-kirienko Aug 25, 2023
96186b6
Refactor things and finish the network layer
pavel-kirienko Aug 27, 2023
e39af2e
Implement rx/tx polling
pavel-kirienko Aug 27, 2023
726ce85
Working IO
pavel-kirienko Aug 29, 2023
428e3da
Implement PnP allocation
pavel-kirienko Aug 29, 2023
9435af3
Fix allocation logic -- anonymous transfers are not deduplicated
pavel-kirienko Aug 29, 2023
a302714
Fix dynamic unsubscription
pavel-kirienko Aug 29, 2023
98f4d28
Respect DSCP
pavel-kirienko Aug 30, 2023
5c389c9
Add RPC service support, but no services are implemented yet
pavel-kirienko Aug 31, 2023
29f4ab8
Implement registers
pavel-kirienko Sep 1, 2023
758774b
Switch libudpard to main
pavel-kirienko Sep 1, 2023
032fc15
Implement uavcan.node.port.List publisher
pavel-kirienko Sep 1, 2023
5083008
Implement ExecuteCommand
pavel-kirienko Sep 1, 2023
9da554d
Implement the data subscriber and publisher
pavel-kirienko Sep 1, 2023
8ed15b8
Update the comments
pavel-kirienko Sep 2, 2023
593b6bd
Add the docs for the libudpard demo
pavel-kirienko Sep 4, 2023
db9d90f
Remove unnecessary branch spec
pavel-kirienko Sep 4, 2023
ab3bfad
Disable the easter egg and add a note about the diagnostic register
pavel-kirienko Sep 4, 2023
c066c49
Flip NO_STATIC_ANALYSIS
pavel-kirienko Sep 7, 2023
c72db5f
{libudpard}/* -> {libudpard_demos}/*
pavel-kirienko Sep 7, 2023
ff3711a
Remove the old libudpard submodule
pavel-kirienko Sep 8, 2023
0334215
Add new libudpard submodule
pavel-kirienko Sep 8, 2023
0d95338
Fix ODR violation
pavel-kirienko Sep 8, 2023
02dc2d9
Fix path in README
pavel-kirienko Sep 8, 2023
c6f2dd9
Add explicit conversion to nfds_t with an overflow check. OPEN_MAX is…
pavel-kirienko Sep 8, 2023
d786093
Where is your dog now?
pavel-kirienko Sep 8, 2023
a9f522f
Make RX socket binding compatible with Windows
pavel-kirienko Sep 9, 2023
bf9d929
Describe the deal with UDPRxAwaitable::user_reference=NULL
pavel-kirienko Sep 15, 2023
2d038c3
aww
pavel-kirienko Sep 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -39,6 +39,9 @@ cmake-build*/
.compiled/
.transpiled/

# Node register files
*.cfg

# IDE and tools
.gdbinit
**/.idea/*
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Expand Up @@ -7,3 +7,6 @@
[submodule "submodules/o1heap"]
path = submodules/o1heap
url = https://github.com/pavel-kirienko/o1heap
[submodule "submodules/libudpard"]
path = submodules/libudpard
url = https://github.com/OpenCyphal/libudpard
37 changes: 37 additions & 0 deletions libudpard_demo/.clang-tidy
@@ -0,0 +1,37 @@
---
Checks: >-
boost-*,
bugprone-*,
cert-*,
clang-analyzer-*,
cppcoreguidelines-*,
google-*,
hicpp-*,
llvm-*,
misc-*,
modernize-*,
performance-*,
portability-*,
readability-*,
-google-readability-todo,
-readability-avoid-const-params-in-decls,
-readability-identifier-length,
-cppcoreguidelines-avoid-magic-numbers,
-bugprone-easily-swappable-parameters,
-llvm-header-guard,
-llvm-include-order,
-cert-dcl03-c,
-hicpp-static-assert,
-misc-static-assert,
-modernize-macro-to-enum,
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
CheckOptions:
- key: readability-function-cognitive-complexity.Threshold
value: '99'
- key: readability-magic-numbers.IgnoredIntegerValues
value: '1;2;3;4;5;8;10;16;20;32;50;60;64;100;128;256;500;512;1000'
WarningsAsErrors: '*'
HeaderFilterRegex: '.*'
AnalyzeTemporaryDtors: false
FormatStyle: file
...
60 changes: 60 additions & 0 deletions libudpard_demo/.idea/dictionaries/pavel.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 98 additions & 0 deletions libudpard_demo/CMakeLists.txt
@@ -0,0 +1,98 @@
# This software is distributed under the terms of the MIT License.
# Copyright (C) OpenCyphal Development Team <opencyphal.org>
# Copyright Amazon.com Inc. or its affiliates.
# SPDX-License-Identifier: MIT
# Author: Pavel Kirienko <pavel@opencyphal.org>

cmake_minimum_required(VERSION 3.20)

project(libudpard_demo C)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
set(submodules "${CMAKE_SOURCE_DIR}/../submodules")

# Set up static analysis.
set(STATIC_ANALYSIS OFF CACHE BOOL "enable static analysis")
if (STATIC_ANALYSIS)
# clang-tidy (separate config files per directory)
find_program(clang_tidy NAMES clang-tidy)
if (NOT clang_tidy)
message(FATAL_ERROR "Could not locate clang-tidy")
endif ()
message(STATUS "Using clang-tidy: ${clang_tidy}")
endif ()

# Forward the revision information to the compiler so that we could expose it at runtime. This is entirely optional.
execute_process(
COMMAND git rev-parse --short=16 HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE vcs_revision_id
OUTPUT_STRIP_TRAILING_WHITESPACE
)
message(STATUS "vcs_revision_id: ${vcs_revision_id}")
add_definitions(
-DVERSION_MAJOR=1
-DVERSION_MINOR=0
-DVCS_REVISION_ID=0x${vcs_revision_id}ULL
-DNODE_NAME="org.opencyphal.demos.libudpard"
)

# Transpile DSDL into C using Nunavut. Install Nunavut as follows: pip install nunavut.
# Alternatively, you can invoke the transpiler manually or use https://nunaweb.opencyphal.org.
find_package(nnvg REQUIRED)
create_dsdl_target( # Generate the support library for generated C headers, which is "nunavut.h".
"nunavut_support"
c
${CMAKE_BINARY_DIR}/transpiled
""
OFF
little
"only"
)
set(dsdl_root_namespace_dirs # List all DSDL root namespaces to transpile here.
${submodules}/public_regulated_data_types/uavcan
${submodules}/public_regulated_data_types/reg
)
foreach (ns_dir ${dsdl_root_namespace_dirs})
get_filename_component(ns ${ns_dir} NAME)
message(STATUS "DSDL namespace ${ns} at ${ns_dir}")
create_dsdl_target(
"dsdl_${ns}" # CMake target name
c # Target language to transpile into
${CMAKE_BINARY_DIR}/transpiled # Destination directory (add it to the includes)
${ns_dir} # Source directory
OFF # Disable variable array capacity override
little # Endianness of the target platform (alternatives: "big", "any")
"never" # Support files are generated once in the nunavut_support target (above)
${dsdl_root_namespace_dirs} # Look-up DSDL namespaces
)
add_dependencies("dsdl_${ns}" nunavut_support)
endforeach ()
include_directories(SYSTEM ${CMAKE_BINARY_DIR}/transpiled) # Make the transpiled headers available for inclusion.
add_definitions(-DNUNAVUT_ASSERT=assert)

# Define the LibUDPard static library build target. No special options are needed to use the library, it's very simple.
add_library(udpard_demo STATIC ${submodules}/libudpard/libudpard/udpard.c)
target_include_directories(udpard_demo INTERFACE SYSTEM ${submodules}/libudpard/libudpard)

# Define the demo application build target and link it with the library.
add_executable(
demo
${CMAKE_SOURCE_DIR}/src/main.c
${CMAKE_SOURCE_DIR}/src/storage.c
${CMAKE_SOURCE_DIR}/src/register.c
${CMAKE_SOURCE_DIR}/src/udp.c
)
target_include_directories(demo PRIVATE ${submodules}/cavl)
target_link_libraries(demo PRIVATE udpard_demo)
add_dependencies(demo dsdl_uavcan dsdl_reg)
set_target_properties(
demo
PROPERTIES
COMPILE_FLAGS "-Wall -Wextra -Werror -pedantic -Wdouble-promotion -Wswitch-enum -Wfloat-equal \
-Wundef -Wconversion -Wtype-limits -Wsign-conversion -Wcast-align -Wmissing-declarations"
C_STANDARD 11
C_EXTENSIONS OFF
)
if (STATIC_ANALYSIS)
set_target_properties(demo PROPERTIES C_CLANG_TIDY "${clang_tidy}")
endif ()
183 changes: 183 additions & 0 deletions libudpard_demo/README.md
@@ -0,0 +1,183 @@
# LibUDPard demo application

This demo application is a usage demonstrator for [LibUDPard](https://github.com/OpenCyphal-Garage/libudpard) ---
a compact Cyphal/UDP implementation for high-integrity systems written in C99.
It implements a simple Cyphal node that showcases the following features:

- Fixed port-ID and non-fixed port-ID publishers.
- Fixed port-ID and non-fixed port-ID subscribers.
- Fixed port-ID RPC server.
- Plug-and-play node-ID allocation unless it is configured statically.
- Fast Cyphal Register API and non-volatile storage for the persistent registers.
- Support for redundant network interfaces.

This document will walk you through the process of building, running, and evaluating the demo
on a GNU/Linux-based OS.
It can be easily ported to another platform, such as a baremetal MCU,
by replacing the POSIX socket API and stdio with suitable alternatives;
for details, please consult with `udp.h` and `storage.h`.

## Preparation

Install the [Yakut](https://github.com/OpenCyphal/yakut) CLI tool,
Wireshark with the [Cyphal plugins](https://github.com/OpenCyphal/wireshark_plugins),
and ensure you have CMake and a C11 compiler.
Build the demo:

```shell
git clone --recursive https://github.com/OpenCyphal/demos
cd demos/libudpard_demo
mkdir build && cd build
cmake .. && make
```

## Running

At the first launch the default parameters will be used.
Upon their modification, the state will be saved on the filesystem in the current working directory
-- you will see a new file appear per parameter (register).

Per the default settings, the node will use only the local loopback interface.
If it were an embedded system, it could be designed to run a DHCP client to configure the local interface(s)
automatically and then use that configuration.

Run the node:

```shell
./demo
```

It will print a few informational messages and then go silent.
With the default configuration being missing, the node will be attempting to perform a plug-and-play node-ID allocation
by sending allocation requests forever until an allocation response is received.
You can see this activity --- PnP requests being published --- using Wireshark;
to exclude unnecessary traffic, use the following BPF expression:

```bpf
udp and dst net 239.0.0.0 mask 255.0.0.0 and dst port 9382
```

<img src="docs/wireshark-pnp.png" alt="Wireshark capture of a PnP request">

It will keep doing this forever until it got an allocation response from the node-ID allocator.
Note that most high-integrity systems would always assign static node-ID instead of relying on this behavior
to ensure deterministic behaviors at startup.

To let our application complete the PnP allocation stage, we launch the PnP allocator implemented in Yakut monitor
(this can be done in any working directory):

```shell
UAVCAN__UDP__IFACE="127.0.0.1" UAVCAN__NODE__ID=$(yakut accommodate) y mon -P allocation_table.db
```

This will create a new file called `allocation_table.db` containing, well, the node-ID allocation table.
Once the allocation is done (it takes a couple of seconds), the application will announce this by printing a message,
and then the normal operation will be commenced.
The Yakut monitor will display the following picture:

<img src="docs/yakut-monitor-pnp.png" alt="Yakut monitor output after PnP allocation">

The newly allocated node-ID value will be stored in the `uavcan.node.id` register,
but it will not be committed into the non-volatile storage until the node is commanded to restart.
This is because storage I/O is not compatible with real-time execution,
so the storage is only accessed during startup of the node (to read the values from the non-volatile memory)
and immediately before shutdown (to commit values into the non-volatile memory).

It is best to keep the Yakut monitor running and execute all subsequent commands in other shell sessions.

Suppose we want the node to use another network interface aside from the local loopback `127.0.0.1`.
This is done by entering additional local interface addresses in the `uavcan.udp.iface` register separated by space.
You can do this with the help of Yakut as follows (here we are assuming that the allocated node-ID is 65532):

```shell
export UAVCAN__UDP__IFACE="127.0.0.1" # Pro tip: move these export statements into a shell file and source it.
export UAVCAN__NODE__ID=$(yakut accommodate)
y r 65532 uavcan.udp.iface "127.0.0.1 192.168.1.200" # Update the local addresses to match your setup.
```

To let the new configuration take effect, the node has to be restarted.
Before restart the register values will be committed into the non-volatile storage;
configuration files will appear in the current working directory of the application.

```shell
y cmd 65532 restart
```

The Wireshark capture will show that the node is now sending data via two interfaces concurrently
(or however many you configured).

Next we will evaluate the application-specific publisher and subscriber.
The application can receive messages of type `uavcan.primitive.array.Real32.1`
and re-publish them with the reversed order of the elements.
The corresponding publisher and subscriber ports are both named `my_data`,
and their port-ID registers are named `uavcan.pub.my_data.id` and `uavcan.sub.my_data.id`,
per standard convention.
As these ports are not configured yet, the node is unable to make use of them.
Configure them manually as follows
(you can also use a YAML file with the `y rb` command for convenience; for more info see Yakut documentation):

```shell
y r 65532 uavcan.pub.my_data.id 1001 # You can pick arbitrary values here.
y r 65532 uavcan.sub.my_data.id 1002
```

Then restart the node, and the Yakut monitor will display the newly configured ports in the connectivity matrix:

<img src="docs/yakut-monitor-data.png" alt="Yakut monitor showing the data topics in the connectivity matrix">

To evaluate this part of the application,
subscribe to the topic it is supposed to publish on using the Yakut subscriber tool:

```shell
y sub +M 1001:uavcan.primitive.array.real32
```

Use another terminal to publish some data on the topic that the application is subscribed to
(you can use a joystick to generate data dynamically; see the Yakut documentation for more info):

```shell
y pub 1002:uavcan.primitive.array.real32 '[50, 60, 70]'
```

The subscriber we launched earlier will show the messages published by our node:

```yaml
1001:
_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}
value: [70.0, 60.0, 50.0]
```

The current status of the memory allocators can be checked by reading the corresponding diagnostic register
as shown below.
Diagnostic registers are a powerful tool for building advanced introspection interfaces and can be used to expose
and even modify arbitrary internal states of the application over the network.

```yaml
y r 65532 sys.info.mem
```

Publishing and subscribing using different remote machines (instead of using the local loopback interface)
is left as an exercise to the reader.
Cyphal/UDP is a masterless peer protocol that does not require manual configuration of the networking infrastructure.
As long as the local interface addresses are set correctly, the Cyphal distributed system will just work out of the box.
Thus, you can simply run the same publisher and subscriber commands on different computers
and see the demo node behave the same way.

The only difference to keep in mind is that Yakut monitor may fail to display all network traffic unless the computer
it is running on is connected to a SPAN port (mirrored port) of the network switch.
This is because by default, IGMP-compliant switches will not forward multicast packets into ports for which
no members for the corresponding multicast groups are registered.

To reset the node configuration back to defaults, use this:

```shell
y cmd 65532 factory_reset
```

As the application is not allowed to access the storage I/O during runtime,
the factory reset will not take place until the node is restarted.
Once restarted, the configuration files will disappear from the current working directory.

## Porting

Just read the code. Focus your attention on `udp.c` and `storage.c`.