Skip to content

Commit 6e72fe5

Browse files
Amend the RX RPC API and populate README.md & CONTRIBUTING.md (#53)
1 parent a6980ac commit 6e72fe5

File tree

6 files changed

+152
-80
lines changed

6 files changed

+152
-80
lines changed

CONTRIBUTING.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
1-
# WORK IN PROGRESS, NOT READY FOR USE
1+
# LibUDPard contribution guidelines
22

3-
While this is a work in progress, contribute via the forums at [https://forum.opencyphal.org/](https://forum.opencyphal.org/)
3+
## Standards
4+
5+
The library shall be implemented in ISO C99/C11 following MISRA C:2012.
6+
The MISRA compliance is enforced by Clang-Tidy and SonarQube.
7+
Deviations are documented directly in the source code as follows:
8+
9+
```c
10+
// Intentional violation of MISRA: <some valid reason>
11+
<... deviant construct ...>
12+
```
13+
14+
The full list of deviations with the accompanying explanation can be found by grepping the sources.
15+
16+
Do not suppress compliance warnings using the means provided by static analysis tools because such deviations
17+
are impossible to track at the source code level.
18+
An exception applies for the case of false-positive (invalid) warnings -- those should not be mentioned in the codebase.
19+
20+
Unfortunately, some rules are hard or impractical to enforce automatically,
21+
so code reviewers shall be aware of MISRA and general high-reliability coding practices
22+
to prevent non-compliant code from being accepted into upstream.
23+
24+
## Build & test
25+
26+
Consult with the CI workflow files for the required tools and build & test instructions.
27+
You may want to use the [toolshed](https://github.com/OpenCyphal/docker_toolchains/pkgs/container/toolshed)
28+
container for this.
29+
30+
## Releasing
31+
32+
Simply create a new release & tag on GitHub.

README.md

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,63 @@
1-
# NOTICE
2-
3-
This package is a staging package to make changes before committing a pull request for the github repo: https://github.com/OpenCyphal-Garage/libudpard based on @schoberm's prototype work
4-
5-
# Compact Cyphal/UDP v1 in C
1+
# Compact Cyphal/UDP in C
62

73
[![Main Workflow](https://github.com/OpenCyphal-Garage/libudpard/actions/workflows/main.yml/badge.svg)](https://github.com/OpenCyphal-Garage/libudpard/actions/workflows/main.yml)
84
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=libudpard&metric=reliability_rating)](https://sonarcloud.io/summary?id=libudpard)
95
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=libudpard&metric=coverage)](https://sonarcloud.io/summary?id=libudpard)
106
[![Forum](https://img.shields.io/discourse/users.svg?server=https%3A%2F%2Fforum.opencyphal.org&color=1700b3)](https://forum.opencyphal.org)
117

12-
LibUDPard is a compact implementation of the Cyphal/UDP protocol stack in C99/C11 for high-integrity real-time
8+
LibUDPard is a compact implementation of the Cyphal/UDP protocol in C99/C11 for high-integrity real-time
139
embedded systems.
1410

1511
[Cyphal](https://opencyphal.org) is an open lightweight data bus standard designed for reliable intravehicular
1612
communication in aerospace and robotic applications via CAN bus, UDP, and other robust transports.
1713

1814
We pronounce LibUDPard as *lib-you-dee-pee-ard*.
1915

20-
## WORK IN PROGRESS, NOT READY FOR FORMAL USE
16+
## Features
17+
18+
Some of the features listed here are intrinsic properties of Cyphal.
19+
20+
- Full branch coverage and extensive static analysis.
21+
22+
- Compliance with automatically enforceable MISRA C rules (reach out to https://forum.opencyphal.org for details).
23+
24+
- Detailed time complexity and memory requirement models for the benefit of real-time high-integrity applications.
25+
26+
- Purely reactive time-deterministic API without the need for background servicing.
27+
28+
- Zero-copy data pipeline on reception --
29+
payload is moved from the underlying NIC driver all the way to the application without copying.
2130

22-
**Read the docs in [`libudpard/udpard.h`](/libudpard/udpard.h).**
31+
- Support for redundant network interfaces with seamless interface aggregation and no fail-over delay.
2332

24-
Building
25-
```
26-
cmake -B ./build -DCMAKE_BUILD_TYPE=Debug -DNO_STATIC_ANALYSIS=1 -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_EXPORT_COMPILE_COMMANDS=1 tests
27-
```
28-
Testing
29-
```
30-
cd build
31-
make
32-
make test
33-
```
34-
Or to debug
35-
```
36-
TEST_OUTPUT_ON_FAILURE=TRUE make test
37-
```
33+
- Out-of-order multi-frame transfer reassembly, including cross-transfer interleaved frames.
3834

39-
## Features, Description, and Usage
35+
- Support for repetition-coding forward error correction (FEC) for lossy links (e.g., wireless)
36+
transparent to the application.
4037

41-
To be added at a later date.
38+
- No dependency on heap memory; the library can be used with fixed-size block pool allocators.
39+
40+
- Compatibility with all conventional 8/16/32/64-bit platforms.
41+
42+
- Compatibility with extremely resource-constrained baremetal environments starting from 64K ROM and 64K RAM.
43+
44+
- Implemented in ≈2000 lines of code.
45+
46+
## Usage
47+
48+
The library implements the Cyphal/UDP protocol, which is a transport-layer entity.
49+
An application using this library will need to implement the presentation layer above the library,
50+
perhaps with the help of the [Nunavut transpiler](https://github.com/OpenCyphal/nunavut),
51+
and the network layer below the library using a third-party UDP/IP stack implementation with multicast/IGMP support
52+
(TCP and ARP are not needed).
53+
In the most straightforward case, the network layer can be based on the standard Berkeley socket API
54+
or a lightweight embedded stack such as LwIP.
55+
56+
**Read the API docs in [`libudpard/udpard.h`](libudpard/udpard.h).**
57+
For complete usage examples, please refer to <https://github.com/OpenCyphal-Garage/demos>.
4258

4359
## Revisions
44-
### v0.0
4560

46-
Prototype commit
61+
### v1.0
62+
63+
Initial release.

libudpard/udpard.c

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,19 +1721,32 @@ int_fast8_t udpardRxSubscriptionReceive(struct UdpardRxSubscription* const self,
17211721
}
17221722

17231723
int_fast8_t udpardRxRPCDispatcherInit(struct UdpardRxRPCDispatcher* const self,
1724-
const UdpardNodeID local_node_id,
17251724
const struct UdpardRxMemoryResources memory)
17261725
{
17271726
int_fast8_t result = -UDPARD_ERROR_ARGUMENT;
1728-
if ((self != NULL) && (local_node_id <= UDPARD_NODE_ID_MAX) && rxValidateMemoryResources(memory))
1727+
if ((self != NULL) && rxValidateMemoryResources(memory))
17291728
{
17301729
memZero(sizeof(*self), self);
1731-
self->local_node_id = local_node_id;
1732-
self->udp_ip_endpoint = makeServiceUDPIPEndpoint(local_node_id);
1733-
self->memory = memory;
1734-
self->request_ports = NULL;
1735-
self->response_ports = NULL;
1736-
result = 0;
1730+
self->local_node_id = UDPARD_NODE_ID_UNSET;
1731+
self->memory = memory;
1732+
self->request_ports = NULL;
1733+
self->response_ports = NULL;
1734+
result = 0;
1735+
}
1736+
return result;
1737+
}
1738+
1739+
int_fast8_t udpardRxRPCDispatcherStart(struct UdpardRxRPCDispatcher* const self,
1740+
const UdpardNodeID local_node_id,
1741+
struct UdpardUDPIPEndpoint* const out_udp_ip_endpoint)
1742+
{
1743+
int_fast8_t result = -UDPARD_ERROR_ARGUMENT;
1744+
if ((self != NULL) && (out_udp_ip_endpoint != NULL) && (local_node_id <= UDPARD_NODE_ID_MAX) &&
1745+
(self->local_node_id > UDPARD_NODE_ID_MAX))
1746+
{
1747+
self->local_node_id = local_node_id;
1748+
*out_udp_ip_endpoint = makeServiceUDPIPEndpoint(local_node_id);
1749+
result = 0;
17371750
}
17381751
return result;
17391752
}

libudpard/udpard.h

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -915,29 +915,14 @@ struct UdpardRxRPCPort
915915
};
916916

917917
/// A service dispatcher is a collection of RPC-service RX ports.
918-
///
919-
/// In Cyphal/UDP, each node has a specific IP multicast group address where RPC-service transfers destined to that
920-
/// node are sent to. This is similar to subject (topic) multicast group addressed except that the node-ID takes
921-
/// the place of the subject-ID. The IP multicast group address is derived from the local node-ID.
922-
/// This address is available in the field named "udp_ip_endpoint".
923-
/// The application is expected to open a separate socket bound to that endpoint per redundant interface,
924-
/// and then feed the UDP datagrams received from these sockets into udpardRxRPCDispatcherReceive,
925-
/// collecting UdpardRxRPCTransfer instances at the output.
926-
///
927918
/// Anonymous nodes (nodes without a node-ID of their own) cannot use RPC-services.
928919
struct UdpardRxRPCDispatcher
929920
{
930921
/// The local node-ID has to be stored to facilitate correctness checking of incoming transfers.
931-
/// This value shall not be modified after initialization. If the local node needs to change its node-ID,
932-
/// this dispatcher instance must be destroyed and a new one created instead.
922+
/// This value shall not be modified.
933923
/// READ-ONLY
934924
UdpardNodeID local_node_id;
935925

936-
/// The IP address and UDP port number where UDP/IP datagrams carrying RPC-service transfers destined to this node
937-
/// will be sent.
938-
/// READ-ONLY
939-
struct UdpardUDPIPEndpoint udp_ip_endpoint;
940-
941926
/// Refer to UdpardRxMemoryResources.
942927
struct UdpardRxMemoryResources memory;
943928

@@ -956,23 +941,29 @@ struct UdpardRxRPCTransfer
956941

957942
/// To begin receiving RPC-service requests and/or responses, the application should do this:
958943
///
959-
/// 1. Create a new UdpardRxRPCDispatcher instance.
944+
/// 1. Create a new UdpardRxRPCDispatcher instance and initialize it by calling udpardRxRPCDispatcherInit.
960945
///
961-
/// 2. Initialize it by calling udpardRxRPCDispatcherInit. Observe that a valid node-ID is required here.
962-
/// If the application has to perform a plug-and-play node-ID allocation, it has to complete that beforehand.
963-
/// The dispatcher is not needed to perform PnP node-ID allocation.
946+
/// 2. Announce its interest in specific RPC-services (requests and/or responses) by calling
947+
/// udpardRxRPCDispatcherListen per each. This can be done at any later point as well.
964948
///
965-
/// 3. Per redundant network interface:
966-
/// - Create a new socket bound to the IP multicast group address and UDP port number specified in the
967-
/// udp_ip_endpoint field of the initialized RPC dispatcher instance. The library will determine the
968-
/// endpoint to use based on the node-ID.
949+
/// 3. When the local node-ID is known, invoke udpardRxRPCDispatcherStart to inform the library of the
950+
/// node-ID value of the local node, and at the same time obtain the address of the UDP/IP multicast group
951+
/// to bind the socket(s) to. This step can be taken before or after the RPC-service port registration.
952+
/// If the application has to perform a plug-and-play node-ID allocation, it has to complete that beforehand
953+
/// (the dispatcher is not needed for PnP node-ID allocation).
969954
///
970-
/// 4. Announce its interest in specific RPC-services (requests and/or responses) by calling
971-
/// udpardRxRPCDispatcherListen per each. This can be done at any later point as well.
955+
/// 4. Having obtained the UDP/IP endpoint in the previous step, do per redundant network interface:
956+
/// - Create a new socket bound to the IP multicast group address and UDP port number obtained earlier.
957+
/// The multicast group address depends on the local node-ID.
972958
///
973959
/// 5. Read data from the sockets continuously and forward each received UDP datagram to
974960
/// udpardRxRPCDispatcherReceive, along with the index of the redundant interface
975-
/// the datagram was received on. Only those services that were announced in step 4 will be processed.
961+
/// the datagram was received on. Only those services that were announced in step 3 will be processed.
962+
///
963+
/// The reason the local node-ID has to be specified via a separate call is to allow the application to set up the
964+
/// RPC ports early, without having to be aware of its own node-ID. This is useful for applications that perform
965+
/// plug-and-play node-ID allocation. Applications where PnP is not needed will simply call both functions
966+
/// at the same time during early initialization.
976967
///
977968
/// There is no resource deallocation function ("free") for the RPC dispatcher. This is because the dispatcher
978969
/// does not own any resources. To dispose of a dispatcher safely, the application shall invoke
@@ -983,9 +974,31 @@ struct UdpardRxRPCTransfer
983974
///
984975
/// The time complexity is constant. This function does not invoke the dynamic memory manager.
985976
int_fast8_t udpardRxRPCDispatcherInit(struct UdpardRxRPCDispatcher* const self,
986-
const UdpardNodeID local_node_id,
987977
const struct UdpardRxMemoryResources memory);
988978

979+
/// This function must be called exactly once to complete the initialization of the RPC dispatcher.
980+
/// It takes the node-ID of the local node, which is used to derive the UDP/IP multicast group address
981+
/// to bind the sockets to, which is returned via the out parameter.
982+
///
983+
/// In Cyphal/UDP, each node has a specific IP multicast group address where RPC-service transfers destined to that
984+
/// node are sent to. This is similar to subject (topic) multicast group addressed except that the node-ID takes
985+
/// the place of the subject-ID. The IP multicast group address is derived from the local node-ID.
986+
///
987+
/// The application is expected to open a separate socket bound to that endpoint per redundant interface,
988+
/// and then feed the UDP datagrams received from these sockets into udpardRxRPCDispatcherReceive,
989+
/// collecting UdpardRxRPCTransfer instances at the output.
990+
///
991+
/// This function shall not be called more than once per dispatcher. If the local node needs to change its node-ID,
992+
/// this dispatcher instance must be destroyed and a new one created instead.
993+
///
994+
/// The return value is 0 on success.
995+
/// The return value is a negated UDPARD_ERROR_ARGUMENT if any of the input arguments are invalid.
996+
///
997+
/// The time complexity is constant. This function does not invoke the dynamic memory manager.
998+
int_fast8_t udpardRxRPCDispatcherStart(struct UdpardRxRPCDispatcher* const self,
999+
const UdpardNodeID local_node_id,
1000+
struct UdpardUDPIPEndpoint* const out_udp_ip_endpoint);
1001+
9891002
/// This function lets the application register its interest in a particular service-ID and kind (request/response)
9901003
/// by creating an RPC-service RX port. The port pointer shall retain validity until its unregistration or until
9911004
/// the dispatcher is destroyed. The service instance shall not be moved or destroyed.

tests/src/test_e2e.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,9 @@ void testRPC()
427427
}
428428
// Initialize the RPC dispatcher and the RPC services.
429429
UdpardRxRPCDispatcher dispatcher{};
430-
TEST_ASSERT_EQUAL(0, udpardRxRPCDispatcherInit(&dispatcher, 4321, mem_rx));
430+
TEST_ASSERT_EQUAL(0, udpardRxRPCDispatcherInit(&dispatcher, mem_rx));
431+
UdpardUDPIPEndpoint udp_ip_endpoint{};
432+
TEST_ASSERT_EQUAL(0, udpardRxRPCDispatcherStart(&dispatcher, 4321, &udp_ip_endpoint));
431433
UdpardRxRPCPort port_foo_a{};
432434
UdpardRxRPCPort port_foo_q{};
433435
TEST_ASSERT_EQUAL(1, udpardRxRPCDispatcherListen(&dispatcher, &port_foo_a, 200, false, 500));
@@ -473,7 +475,7 @@ void testRPC()
473475
TEST_ASSERT_EQUAL(0, alloc_rx_payload.allocated_fragments);
474476
const UdpardTxItem* tx_item = udpardTxPeek(&tx);
475477
TEST_ASSERT_NOT_NULL(tx_item);
476-
TEST_ASSERT_EQUAL(dispatcher.udp_ip_endpoint.ip_address, tx_item->destination.ip_address);
478+
TEST_ASSERT_EQUAL(udp_ip_endpoint.ip_address, tx_item->destination.ip_address);
477479
TEST_ASSERT_NULL(tx_item->next_in_transfer);
478480
TEST_ASSERT_EQUAL(10'001'000, tx_item->deadline_usec);
479481
TEST_ASSERT_EQUAL(0xA1, tx_item->dscp);
@@ -533,7 +535,7 @@ void testRPC()
533535
// Second transfer.
534536
tx_item = udpardTxPeek(&tx);
535537
TEST_ASSERT_NOT_NULL(tx_item);
536-
TEST_ASSERT_EQUAL(dispatcher.udp_ip_endpoint.ip_address, tx_item->destination.ip_address);
538+
TEST_ASSERT_EQUAL(udp_ip_endpoint.ip_address, tx_item->destination.ip_address);
537539
TEST_ASSERT_NULL(tx_item->next_in_transfer);
538540
TEST_ASSERT_EQUAL(10'000'000, tx_item->deadline_usec);
539541
TEST_ASSERT_EQUAL(0xA2, tx_item->dscp);

tests/src/test_rx.cpp

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -209,25 +209,15 @@ void testRxRPCDispatcher()
209209

210210
// Initialize the RPC dispatcher.
211211
UdpardRxRPCDispatcher self{};
212-
TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT,
213-
udpardRxRPCDispatcherInit(nullptr,
214-
0xFFFFU,
215-
{
216-
.session = instrumentedAllocatorMakeMemoryResource(&mem_session),
217-
.fragment = instrumentedAllocatorMakeMemoryResource(&mem_fragment),
218-
.payload = instrumentedAllocatorMakeMemoryDeleter(&mem_payload),
219-
}));
220212
TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT,
221213
udpardRxRPCDispatcherInit(&self,
222-
0x1042,
223214
{
224215
.session = {nullptr, nullptr, nullptr},
225216
.fragment = instrumentedAllocatorMakeMemoryResource(&mem_fragment),
226217
.payload = instrumentedAllocatorMakeMemoryDeleter(&mem_payload),
227218
}));
228219
TEST_ASSERT_EQUAL(0,
229220
udpardRxRPCDispatcherInit(&self,
230-
0x1042,
231221
{
232222
.session = instrumentedAllocatorMakeMemoryResource(&mem_session),
233223
.fragment = instrumentedAllocatorMakeMemoryResource(&mem_fragment),
@@ -237,13 +227,21 @@ void testRxRPCDispatcher()
237227
TEST_ASSERT_EQUAL(&instrumentedAllocatorDeallocate, self.memory.session.deallocate);
238228
TEST_ASSERT_NULL(self.request_ports);
239229
TEST_ASSERT_NULL(self.response_ports);
240-
TEST_ASSERT_EQUAL(0x1042, self.local_node_id);
241-
TEST_ASSERT_EQUAL(0xEF011042UL, self.udp_ip_endpoint.ip_address);
242-
TEST_ASSERT_EQUAL(9382, self.udp_ip_endpoint.udp_port);
230+
TEST_ASSERT_EQUAL(0xFFFF, self.local_node_id);
243231
TEST_ASSERT_EQUAL(0, mem_session.allocated_fragments);
244232
TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments);
245233
TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments);
246234

235+
// Start the dispatcher by setting the local node ID.
236+
UdpardUDPIPEndpoint udp_ip_endpoint{};
237+
TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardRxRPCDispatcherStart(&self, 0xFFFF, &udp_ip_endpoint));
238+
TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardRxRPCDispatcherStart(&self, 0x1042, nullptr));
239+
TEST_ASSERT_EQUAL(0, udpardRxRPCDispatcherStart(&self, 0x1042, &udp_ip_endpoint));
240+
TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardRxRPCDispatcherStart(&self, 0x1042, &udp_ip_endpoint));
241+
TEST_ASSERT_EQUAL(0x1042, self.local_node_id);
242+
TEST_ASSERT_EQUAL(0xEF011042UL, udp_ip_endpoint.ip_address);
243+
TEST_ASSERT_EQUAL(9382, udp_ip_endpoint.udp_port);
244+
247245
// Add a request port.
248246
UdpardRxRPCPort port_request_foo{};
249247
TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardRxRPCDispatcherListen(&self, nullptr, 511, true, 100));

0 commit comments

Comments
 (0)