diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5bce2c9..342bcc1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ on: [push] jobs: build_ubuntu: runs-on: ubuntu-24.04 - timeout-minutes: 5 + timeout-minutes: 6 strategy: matrix: target: [ubuntu] @@ -20,12 +20,19 @@ jobs: - name: Install run: | - sudo apt-get install -y make gcc-multilib g++-multilib linux-modules-extra-$(uname -r) + sudo apt-get install -y gcc-multilib g++-multilib linux-modules-extra-$(uname -r) python3 -m venv .venv && source ".venv/bin/activate" - pip install -r requirements.txt + pip install -r requirements-dev.txt - - name: Build and run SITL + - name: Build SITL + run: | + source ".venv/bin/activate" + make ${{ matrix.target }}-build + + - name: Run SITL and smoke test + timeout-minutes: 3 run: | ./scripts/vcan.sh slcan0 source ".venv/bin/activate" - make ${{ matrix.target }} + make ${{ matrix.target }}-run & + ./tests/smoke_socketcan.py diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index d922c62..8e8e4e4 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -4,12 +4,11 @@ on: [push] jobs: cpplint: - if: false # disable temporarily runs-on: ubuntu-24.04 - timeout-minutes: 1 + timeout-minutes: 2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: | pip3 install cpplint sudo apt-get install -y cppcheck dos2unix - - run: make code_style + - run: make cpplint diff --git a/.gitignore b/.gitignore index 378f211..6d95ee8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode/ build/ -.venv \ No newline at end of file +.venv +__pycache__/ diff --git a/CPPLINT.cfg b/CPPLINT.cfg index bf8d7bc..2ec798d 100644 --- a/CPPLINT.cfg +++ b/CPPLINT.cfg @@ -19,6 +19,7 @@ filter=-legal/copyright filter=-whitespace/braces filter=-whitespace/parens filter=-whitespace/indent +filter=-whitespace/comments # This this is not C++, C-style cast is the only way to perform cast filter=-readability/casting diff --git a/Makefile b/Makefile index 37cff68..3ea5f0a 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,35 @@ # Copyright (c) 2023 Dmitry Ponomarev -# Distributed under the MIT License, available in the file LICENSE. +# Distributed under the MPL v2.0 License, available in the file LICENSE. # Author: Dmitry Ponomarev ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) BUILD_DIR:=$(ROOT_DIR)/build BUILD_EXAMPLES_DIR:=$(BUILD_DIR)/src/examples -define build_and_run_sitl +define build_sitl $(info Build example $(1)...) mkdir -p $(BUILD_EXAMPLES_DIR)/$(1) cd $(BUILD_EXAMPLES_DIR)/$(1) && cmake $(ROOT_DIR)/examples/$(1) && make -s +endef + +define run_sitl + $(info Run example $(1)...) $(BUILD_EXAMPLES_DIR)/$(1)/ubuntu endef -ubuntu: - $(call build_and_run_sitl,ubuntu) +.PHONY: ubuntu ubuntu-build ubuntu-run + +ubuntu-build: + $(call build_sitl,ubuntu) + +ubuntu-run: + @if [ ! -x "$(BUILD_EXAMPLES_DIR)/ubuntu/ubuntu" ]; then \ + $(MAKE) ubuntu-build; \ + fi + $(call run_sitl,ubuntu) + +ubuntu: ubuntu-build + $(call run_sitl,ubuntu) clean: rm -rf build/examples/ @@ -23,7 +38,7 @@ code_style: cpplint cppcheck crlf astyle: ./scripts/code_style/check_astyle.py src include --astylerc scripts/code_style/astylerc cpplint: - cpplint src/*.c include/application/*.h include/serialization/*.h include/serialization/*/*/*.h include/serialization/*/*/*/*.h + cpplint src/*.cpp include/libdcnode/*.h include/libdcnode/*.hpp cppcheck: ./scripts/code_style/cppcheck.sh crlf: diff --git a/README.md b/README.md index 662a754..16bb93c 100644 --- a/README.md +++ b/README.md @@ -1,186 +1,55 @@ -[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=PonomarevDA_dronecan_application&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=PonomarevDA_dronecan_application) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=PonomarevDA_dronecan_application&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=PonomarevDA_dronecan_application) [![build](https://github.com/PonomarevDA/dronecan_application/actions/workflows/build.yml/badge.svg)](https://github.com/PonomarevDA/dronecan_application/actions/workflows/build.yml) [![check_crlf](https://github.com/PonomarevDA/dronecan_application/actions/workflows/check_crlf.yml/badge.svg)](https://github.com/PonomarevDA/dronecan_application/actions/workflows/check_crlf.yml) +# libdcnode [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=PonomarevDA_libdcnode&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=PonomarevDA_libdcnode) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=PonomarevDA_libdcnode&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=PonomarevDA_libdcnode) [![build](https://github.com/PonomarevDA/libdcnode/actions/workflows/build.yml/badge.svg)](https://github.com/PonomarevDA/libdcnode/actions/workflows/build.yml) -# DroneCAN application +Lightweight [DroneCAN / UAVCAN v0](https://dronecan.github.io/) node toolkit built on top of [libcanard](https://github.com/dronecan/libcanard), [platform_specific](platform_specific) and [libparams](https://github.com/PonomarevDA/libparams). It wraps the transport, exposes a small C API for node setup/spin, and offers C++ helpers for typed publishers/subscribers and logging. -This is a C library with C++ helpers that brings up the [libcanard](https://github.com/dronecan/libcanard), platform-specific drivers and serialization together to build a minimal DroneCAN application. +## Features -A minimal application includes the following protocol-features: +- Implements core DroneCAN services out of the box: [NodeStatus](https://legacy.uavcan.org/Specification/7._List_of_standard_data_types/#nodestatus), [GetNodeInfo](https://legacy.uavcan.org/Specification/7._List_of_standard_data_types/#getnodeinfo), [Param.GetSet and Param.ExecuteOpcode](https://legacy.uavcan.org/Specification/7._List_of_standard_data_types/#uavcanprotocolparam), [RestartNode](https://legacy.uavcan.org/Specification/7._List_of_standard_data_types/#restartnode), and [GetTransportStats](https://legacy.uavcan.org/Specification/7._List_of_standard_data_types/#gettransportstats). +- Platform abstraction (`include/libdcnode/platform.hpp`) so you can plug in your own timing, reset, unique-ID, and CAN driver callbacks. +- Templated C++ wrappers for publishing/subscribing (`include/libdcnode/pub.hpp`, `include/libdcnode/sub.hpp`) with optional filters and periodic senders; legacy interfaces remain in `include/libdcnode/publisher.hpp` and `include/libdcnode/subscriber.hpp`. +- Debug logger (`include/libdcnode/logger.hpp`) that publishes `uavcan.protocol.debug.LogMessage` with severity helpers. +- DSDL compiler baked into the build (`Libs/dronecan_dsdlc`) so message code is generated during CMake configure. -| № | type | message | -| - | --------- | -------- | -| 1 | broadcast | [uavcan.protocol.NodeStatus](https://legacy.uavcan.org/Specification/7._List_of_standard_data_types/#nodestatus) | -| 2 | RPC-service | [uavcan.protocol.GetNodeInfo](https://legacy.uavcan.org/Specification/7._List_of_standard_data_types/#getnodeinfo) | -| 3 | RPC-service | [uavcan.protocol.param.*](https://legacy.uavcan.org/Specification/7._List_of_standard_data_types/#uavcanprotocolparam) | -| 4 | RPC-service | [uavcan.protocol.RestartNode](https://legacy.uavcan.org/Specification/7._List_of_standard_data_types/#restartnode) | -| 5 | RPC-service | [uavcan.protocol.GetTransportStats](https://legacy.uavcan.org/Specification/7._List_of_standard_data_types/#gettransportstats) | +## Prerequisites -The following auxiliary features should be provided as well: +- C/C++ toolchain with C++17, CMake >= 3.15, Python 3 (for tests/tools). +- Linux SocketCAN stack and `ip` utilities (used by `scripts/vcan.sh`). +- `can-utils` (for monitoring) and `dronecan_gui_tool` are handy when playing with the examples. -- [x] actuator -- [x] airspeed -- [x] baro -- [x] circuit status -- [x] fuel tank -- [x] esc -- [x] ice -- [x] indication -- [x] power -- [x] rangefinder -- [ ] gnss -- [x] mag -- [ ] etc +## Getting started -The library should support the following platforms: -- [x] Ubuntu: socketcan -- [x] stm32f103: BXCAN based on platform specific -- [x] stm32g0: FDCAN based on STM32 HAL -- [ ] stm32f103: DroneCAN/Serial based on STM32 HAL +Set up the Python deps (used by the DSDL generator and tests): -## Dependencies - -- [libparams](https://github.com/PonomarevDA/libparams) v0.8.4 library. -- [dronecan/libcanard](https://github.com/dronecan/libcanard) - -## How to integrate the library into a project - -Add the following lines into CMakeLists.txt of your project: - -```cmake -# 1. libdcnode -add_subdirectory(${ROOT_DIR} ${CMAKE_BINARY_DIR}/libdcnode) - -# 2. libcanver -set(CAN_PLATFORM socketcan) # Options: bxcan, fdcan or socketcan -include(${ROOT_DIR}/platform_specific/${CAN_PLATFORM}/config.cmake) - -# 3. libparams -set(LIBPARAMS_PLATFORM ubuntu) # Options: stm32f103, stm32g0b1, ubuntu -include(libparams.cmake) - -# 4. Application -add_executable(${PROJECT_NAME} - ... - ${DRONECAN_PLATFORM_SOURCES} - ... -) -target_include_directories(${PROJECT_NAME} PRIVATE - ... - ${DRONECAN_PLATFORM_HEADERS} - ... -) -target_link_libraries(${PROJECT_NAME} PRIVATE - libdcnode::libdcnode -) -``` - - -## Usage example - -**1. Initialize** - -Include `dronecan.hpp` header and call `uavcanInitApplication` in the beginning of the application. Call `uavcanSpinOnce` periodically. - -```c++ -// Include dronecan.hpp header file -#include "libdcnode/dronecan.h" - -// Initialize the library somewhere -const uint8_t node_id = 42; -auto init_res = uavcanInitApplication(node_id); -if (init_res < 0) { - // handle error here -} - -// Spin it periodically: -while (true) { - ... - uavcanSpinOnce(); - ... -} -``` - -**2. Add publisher** - -Adding a publisher is very easy. Include `publisher.hpp` header, create an instance of the required publisher and just call `publish` when you need. Here is a BatteryInfo publisher example: - -```c++ -#include "libdcnode/dronecan.h" -#include "libdcnode/publisher.hpp" - -// Create an instance of the publisher -DronecanPublisher battery_info_pub; - -// Modify the message -battery_info_pub.msg.voltage = 10.0f; - -// Publish the message -battery_info_pub.publish(); +```bash +python3 -m venv .venv && source .venv/bin/activate +pip install -r requirements.txt ``` -Alternatively, you can create a periodic publisher: +Then wire the library into your project: -```c++ -const auto PUBLISH_RATE_HZ = 1.0f; -DronecanPeriodicPublisher battery_info_pub(PUBLISH_RATE_HZ); - -while (true) { - ... - battery_info_pub.spinOnce(); - ... -} +```cmake +add_subdirectory(libdcnode) +target_link_libraries(my_app PRIVATE libdcnode::libdcnode) ``` -**3. Add subscriber** - -Adding a subscriber is easy as well. Let's consider a RawCommand subscriber example. Include `subscriber.hpp` header, create a callback for your application and instance of the required subscriber, then initilize it. - -```c++ -// Include necessary header files -#include "libdcnode/dronecan.h" -#include "libdcnode/subscriber.hpp" - -// Add a callback handler function -void rc_callback(const RawCommand_t& msg) { - std::cout << "Get RawCommand with " << (int)msg.size << " commands." << std::endl; -} - -// Add the subscription: -DronecanSubscriber raw_command_sub; -if (raw_command_sub.init(&rc_callback) < 0) { - // handle error -} -``` +During configure, DSDL code is generated into `build/generated/libdcnode/serialization` using `Libs/dronecan_dsdlc/dronecan_dsdlc.py`. If you add custom DSDL namespaces, point `DSDL_IN_DIR` (see `CMakeLists.txt`) to your set or rerun `scripts/code_generation.sh` with your paths. -Sometimes for subscriber you want to specify a filter. For example, you may want to subscribe on a specific command channel or sensor ID. Let's consider an ArrayCommand example with filter that will only pass the messages with actuator ID = 0. - -```c++ -static const uint8_t FILTER_ACTUATOR_ID = 0; - -void ac_callback(const ArrayCommand_t& msg) { - std::cout << "Get ArrayCommand_t with " << msg.size << "commands." << std::endl; -} -bool ac_filter(const ArrayCommand_t& msg) { - for (size_t idx = 0; idx < msg.size; idx++) { - if (msg.commands[idx].actuator_id == FILTER_ACTUATOR_ID) { - return true; - } - } - return false; -} - -DronecanSubscriber array_command_sub; -array_command_sub.init(&ac_callback, &ac_filter); -``` +Provide the platform and params hooks from `include/libdcnode/platform.hpp` and `include/libdcnode/params.hpp` in your application; the Ubuntu example shows minimal implementations. -**Run example** +### Ubuntu example (build, link, run) -You can run a provided example in SITL mode. Just run: +The `examples/ubuntu` target demonstrates the C API plus the modern C++ pub/sub wrappers end-to-end. ```bash -git clone git@github.com:PonomarevDA/dronecan_application.git --recursive +# Prepare a virtual CAN iface once (uses sudo): +scripts/vcan.sh slcan0 + +# Build and run the example: make ubuntu ``` +What it does: sets `uavcan.node.id` (default 50) and `system.name`, wires platform callbacks (time, restart request, UID, CAN driver) in `examples/ubuntu/main.cpp`, subscribes to `esc.RawCommand`/`actuator.ArrayCommand`/`indication.LightsCommand`, and publishes periodic `equipment.power.CircuitStatus` + `equipment.power.BatteryInfo` while serving NodeStatus/GetNodeInfo. + In gui_tool you will see: drawing @@ -189,39 +58,28 @@ In gui_tool you will see: drawing -> You can find the provided SITL application in [examples/ubuntu](examples/ubuntu) folder. - -## Platform specific notes +## Testing -There are a few functions that require an implementation. They are declared in [include/application/internal.h](include/application/internal.h). +```bash +python3 -m venv .venv && source .venv/bin/activate +pip install -r requirements-dev.txt +scripts/vcan.sh slcan0 # once, requires sudo +pytest tests/smoke_socketcan.py +``` -A user must provide the following function implementation: +The smoke tests expect the Ubuntu example (node ID 50 on `slcan0`) to be running; they verify `NodeStatus` and `GetNodeInfo` responses via the `dronecan` Python client. -```c++ -/** - * @return the time in milliseconds since the application started. - * @note This function must be provided by a user! - */ -uint32_t platformSpecificGetTimeMs(); -``` +## Changelog -A user may also provide the implementation of the optional functions. These function have a weak implementation in [src/weak.c](src/weak.c). - -```c++ -/** - * @return whether the request will be processed - * True - the application will be restarted soon. - * False - the restarted is not supported or can't be handled at the moment. - * @note Implementation is recommended, but optional. - */ -bool platformSpecificRequestRestart(); - -/** - * @param[out] out_id - hardware Unique ID - * @note Implementation is recommended, but optional. - */ -void platformSpecificReadUniqueID(uint8_t out_uid[16]); -``` +| Version | ReleaseDate | Note | +| ------- | ------------ | ---- | +| [v0.7.0](https://github.com/PonomarevDA/libdcnode/tree/v0.7.0) | 2025-12-28 | Added DSDL ser/des generator | +| [v0.6.0](https://github.com/PonomarevDA/libdcnode/tree/v0.6.0) | 2025-10-17 | Build model changed: standalone library; platform hooks provided by user (no more source-include mode). | +| [v0.5.0](https://github.com/PonomarevDA/libdcnode/tree/v0.5.0) | 2024-09-26 | Decoupled the platform specific functions from the library | +| [v0.4.0](https://github.com/PonomarevDA/libdcnode/tree/v0.4.0) | 2024-07-29 | Added macro helpers for pub and sub traits | +| [v0.3.*](https://github.com/PonomarevDA/libdcnode/tree/v0.3.0) | 2024-01-06 | Incremental serialization additions | +| [v0.2.0](https://github.com/PonomarevDA/libdcnode/tree/v0.2.0) | 2024-01-06 | Add bxcan and fdcan drivers | +| [v0.1.0](https://github.com/PonomarevDA/libdcnode/tree/v0.1.0) | 2023-12-22 | First public drop; pure C, manual serialization, and source-include usage (include the C sources into your app). | ## License diff --git a/examples/ubuntu/CMakeLists.txt b/examples/ubuntu/CMakeLists.txt index 420f958..d779f3d 100644 --- a/examples/ubuntu/CMakeLists.txt +++ b/examples/ubuntu/CMakeLists.txt @@ -47,7 +47,7 @@ add_definitions(-DAPP_VERSION_MAJOR=0) add_definitions(-DAPP_VERSION_MINOR=1) add_definitions(-DHW_VERSION_MAJOR=0) add_definitions(-DHW_VERSION_MINOR=0) -add_definitions(-DAPP_NODE_NAME="${PROJECT_NAME}") +add_definitions(-DAPP_NODE_NAME="com.example.libdcnode.sitl") add_executable(${PROJECT_NAME} main.cpp diff --git a/include/libdcnode/pub.hpp b/include/libdcnode/pub.hpp index 7dadf2f..e117c26 100644 --- a/include/libdcnode/pub.hpp +++ b/include/libdcnode/pub.hpp @@ -36,27 +36,31 @@ namespace libdcnode struct DronecanPublisherTraits; #define LIBDCNODE_PUB_ENCODE(MessageType) MessageType##_encode -#define LIBDCNODE_PUB_BROADCAST(buffer, Prefix, inout_transfer_id, size) uavcanPublish(Prefix##_SIGNATURE, Prefix##_ID, inout_transfer_id, CANARD_TRANSFER_PRIORITY_MEDIUM, buffer, size) -#define LIBDCNODE_DEFINE_PUB_TRAITS(MessageType, MessagePrefix) \ - template <> \ - struct DronecanPublisherTraits \ - { \ - using EncodeRet = decltype(LIBDCNODE_PUB_ENCODE(MessageType)(std::declval(), std::declval())); \ - static_assert(std::is_same_v, "*_encode() must return uint32_t exactly."); \ - static inline int16_t publish_once(MessageType *msg, uint8_t *inout_transfer_id) \ - { \ - uint8_t buffer[LIBDCNODE_MAX_PUB_MESSAGE_SIZE]; \ - auto bytes_needed = LIBDCNODE_PUB_ENCODE(MessageType)(msg, buffer); \ - if (bytes_needed == 0U || bytes_needed > LIBDCNODE_MAX_PUB_MESSAGE_SIZE) \ - return (int16_t)0; \ - return LIBDCNODE_PUB_BROADCAST(buffer, MessagePrefix, inout_transfer_id, bytes_needed); \ - } \ +#define LIBDCNODE_PUB_BROADCAST(buffer, Prefix, inout_transfer_id, size) \ + uavcanPublish(Prefix##_SIGNATURE, Prefix##_ID, inout_transfer_id, CANARD_TRANSFER_PRIORITY_MEDIUM, \ + buffer, size) +#define LIBDCNODE_DEFINE_PUB_TRAITS(MessageType, MessagePrefix) \ + template <> \ + struct DronecanPublisherTraits \ + { \ + using EncodeRet = decltype(LIBDCNODE_PUB_ENCODE(MessageType)( \ + std::declval(), std::declval())); \ + static_assert(std::is_same_v, "*_encode() must return uint32_t exactly."); \ + static inline int16_t publish_once(MessageType *msg, uint8_t *inout_transfer_id) \ + { \ + uint8_t buffer[LIBDCNODE_MAX_PUB_MESSAGE_SIZE]; \ + auto bytes_needed = LIBDCNODE_PUB_ENCODE(MessageType)(msg, buffer); \ + if (bytes_needed == 0U || bytes_needed > LIBDCNODE_MAX_PUB_MESSAGE_SIZE) \ + return (int16_t)0; \ + return LIBDCNODE_PUB_BROADCAST(buffer, MessagePrefix, inout_transfer_id, bytes_needed); \ + } \ }; LIBDCNODE_DEFINE_PUB_TRAITS(::uavcan_equipment_power_CircuitStatus, UAVCAN_EQUIPMENT_POWER_CIRCUITSTATUS) LIBDCNODE_DEFINE_PUB_TRAITS(::uavcan_equipment_power_BatteryInfo, UAVCAN_EQUIPMENT_POWER_BATTERYINFO) LIBDCNODE_DEFINE_PUB_TRAITS(::uavcan_equipment_ahrs_RawIMU, UAVCAN_EQUIPMENT_AHRS_RAWIMU) - LIBDCNODE_DEFINE_PUB_TRAITS(::uavcan_equipment_ahrs_MagneticFieldStrength2, UAVCAN_EQUIPMENT_AHRS_MAGNETICFIELDSTRENGTH2) + LIBDCNODE_DEFINE_PUB_TRAITS(::uavcan_equipment_ahrs_MagneticFieldStrength2, + UAVCAN_EQUIPMENT_AHRS_MAGNETICFIELDSTRENGTH2) LIBDCNODE_DEFINE_PUB_TRAITS(::uavcan_equipment_camera_gimbal_Status, UAVCAN_EQUIPMENT_CAMERA_GIMBAL_STATUS) template @@ -81,8 +85,11 @@ namespace libdcnode class DronecanPeriodicPub : public DronecanPub { public: - explicit DronecanPeriodicPub(float frequency) : DronecanPub(), - _pub_period_ms(static_cast(1000.0f / std::clamp(frequency, 0.001f, 1000.0f))) {} + explicit DronecanPeriodicPub(float frequency) + : DronecanPub(), + _pub_period_ms(static_cast( + 1000.0f / std::clamp(frequency, 0.001f, 1000.0f))) + {} inline void spinOnce() { diff --git a/include/libdcnode/publisher.hpp b/include/libdcnode/publisher.hpp index d6a19ea..23b13b0 100644 --- a/include/libdcnode/publisher.hpp +++ b/include/libdcnode/publisher.hpp @@ -92,8 +92,9 @@ template class DronecanPeriodicPublisher : public DronecanPublisher { public: - DronecanPeriodicPublisher(float frequency) : DronecanPublisher(), - PUB_PERIOD_MS(static_cast(1000.0f / std::clamp(frequency, 0.001f, 1000.0f))) {}; + explicit DronecanPeriodicPublisher(float frequency) : + DronecanPublisher(), + PUB_PERIOD_MS(static_cast(1000.0f / std::clamp(frequency, 0.001f, 1000.0f))) {}; inline void spinOnce() { diff --git a/include/libdcnode/sub.hpp b/include/libdcnode/sub.hpp index adbf131..b509ed3 100644 --- a/include/libdcnode/sub.hpp +++ b/include/libdcnode/sub.hpp @@ -50,7 +50,8 @@ namespace libdcnode UAVCAN_EQUIPMENT_HARDPOINT_COMMAND) LIBDCNODE_DEFINE_SUB_TRAITS(::uavcan_equipment_indication_LightsCommand, UAVCAN_EQUIPMENT_INDICATION_LIGHTSCOMMAND) LIBDCNODE_DEFINE_SUB_TRAITS(::uavcan_equipment_actuator_ArrayCommand, UAVCAN_EQUIPMENT_ACTUATOR_ARRAYCOMMAND) - LIBDCNODE_DEFINE_SUB_TRAITS(::uavcan_equipment_camera_gimbal_AngularCommand, UAVCAN_EQUIPMENT_CAMERA_GIMBAL_ANGULARCOMMAND) + LIBDCNODE_DEFINE_SUB_TRAITS(::uavcan_equipment_camera_gimbal_AngularCommand, + UAVCAN_EQUIPMENT_CAMERA_GIMBAL_ANGULARCOMMAND) LIBDCNODE_DEFINE_SUB_TRAITS(::uavcan_equipment_ahrs_Solution, UAVCAN_EQUIPMENT_AHRS_SOLUTION) LIBDCNODE_DEFINE_SUB_TRAITS(::uavcan_equipment_esc_RawCommand, UAVCAN_EQUIPMENT_ESC_RAWCOMMAND) LIBDCNODE_DEFINE_SUB_TRAITS(::uavcan_equipment_safety_ArmingStatus, UAVCAN_EQUIPMENT_SAFETY_ARMINGSTATUS) @@ -94,8 +95,8 @@ namespace libdcnode static inline std::array instances{}; static inline MessageType msg = {}; - void (*user_callback)(const MessageType &){nullptr}; - bool (*filter)(const MessageType &){nullptr}; + void (*user_callback)(const MessageType &) = nullptr; + bool (*filter)(const MessageType &) = nullptr; }; } // namespace libdcnode diff --git a/include/libdcnode/subscriber.hpp b/include/libdcnode/subscriber.hpp index 6ad4384..2743744 100644 --- a/include/libdcnode/subscriber.hpp +++ b/include/libdcnode/subscriber.hpp @@ -98,8 +98,8 @@ class DronecanSubscriber static inline std::array instances{}; static inline MessageType msg = {}; - void (*user_callback)(const MessageType &){nullptr}; - bool (*filter)(const MessageType &){nullptr}; + void (*user_callback)(const MessageType &) = nullptr; + bool (*filter)(const MessageType &) = nullptr; }; #endif // LIBDCNODE_SUBSCRIBER_HPP_ diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..1f5ce5a --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +-r requirements.txt +pytest +pytest-dependency diff --git a/src/dronecan.cpp b/src/dronecan.cpp index bc9fdc8..8b71d18 100644 --- a/src/dronecan.cpp +++ b/src/dronecan.cpp @@ -277,14 +277,11 @@ static uint8_t uavcanProcessSending() canardPopTxQueue(&node.g_canard); txf = canardPeekTxQueue(&node.g_canard); tx_frames_counter++; - } - else if (tx_res < 0) - { + } else if (tx_res < 0) { break; } - if ((tx_attempt++) > 20) - { + if ((tx_attempt++) > 20) { break; } } @@ -302,9 +299,7 @@ static bool uavcanProcessReceiving(uint32_t crnt_time_ms) { uint64_t crnt_time_us = crnt_time_ms * 1000UL; canardHandleRxFrame(&node.g_canard, &rx_frame, crnt_time_us); - } - else - { + } else { break; } } @@ -376,9 +371,7 @@ static void uavcanProtocolParamGetSetHandle(CanardRxTransfer *transfer) if (param_name_length) { param_idx = params.find(recv_name, param_name_length); - } - else - { + } else { param_idx = uavcanParamGetSetDecodeIndex(transfer); } @@ -401,18 +394,14 @@ static void uavcanProtocolParamGetSetHandle(CanardRxTransfer *transfer) params.integer.getMin(param_idx), params.integer.getMax(param_idx), name); - } - else if (params.isString(param_idx)) - { + } else if (params.isString(param_idx)) { if (set_value_type_tag == PARAM_VALUE_STRING) { params.string.setValue(param_idx, str_len, val_string); } const char *str_value = (const char *)params.string.getValue(param_idx); len = uavcanParamGetSetMakeStringResponse(resp, str_value, name); - } - else - { + } else { len = uavcanParamGetSetMakeEmptyResponse(resp); } diff --git a/src/logger.cpp b/src/logger.cpp index 38ea643..e9f0300 100644 --- a/src/logger.cpp +++ b/src/logger.cpp @@ -1,7 +1,9 @@ /* -* Copyright (C) 2024 Anastasiia Stepanova -* Distributed under the terms of the GPL v3 license, available in the file LICENSE. -*/ + * Copyright (C) 2024 Anastasiia Stepanova + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ #include "libdcnode/logger.hpp" #include "libdcnode/legacy/uavcan/protocol/debug/LogMessage.h" diff --git a/tests/smoke_socketcan.py b/tests/smoke_socketcan.py new file mode 100755 index 0000000..37ae6d7 --- /dev/null +++ b/tests/smoke_socketcan.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +from pathlib import Path +import dronecan +import pytest + +APP_NODE_ID = 50 +APP_IFACE = "slcan0" +NODE_ID_TESTER = 100 +MESSAGE_TIMEOUT = 1.2 +SERVICE_TIMEOUT = 0.2 + +def make_dronecan_node() -> dronecan.node.Node: + node = dronecan.make_node(APP_IFACE, node_id=NODE_ID_TESTER, bitrate=1_000_000, baudrate=1_000_000) + node.mode = dronecan.uavcan.protocol.NodeStatus().MODE_OPERATIONAL + node.health = dronecan.uavcan.protocol.NodeStatus().HEALTH_OK + return node + +@pytest.mark.dependency() +def test_node_status(): + node = make_dronecan_node() + + node_status_transfer_event = None + def _on_node_status(transfer_event: dronecan.node.TransferEvent): + if transfer_event is None or transfer_event.transfer.source_node_id != APP_NODE_ID: + return # ignore other nodes if present + nonlocal node_status_transfer_event + node_status_transfer_event = transfer_event + + node.add_handler(dronecan.uavcan.protocol.NodeStatus, _on_node_status) + node.spin(MESSAGE_TIMEOUT) + assert node_status_transfer_event is not None, ( + f"No NodeStatus from node {APP_NODE_ID} on iface {APP_IFACE}. " + "Hint: ensure the SITL app is running (make ubuntu) and slcan0 is up (scripts/vcan.sh slcan0)." + ) + assert node_status_transfer_event.transfer.source_node_id == APP_NODE_ID + assert node_status_transfer_event.message.health == 0 # HEALTH_OK + assert node_status_transfer_event.message.mode == 0 # MODE_OPERATIONAL + +@pytest.mark.dependency(depends=["test_node_status"]) +def test_get_node_info_request(): + node = make_dronecan_node() + + req = dronecan.uavcan.protocol.GetNodeInfo.Request() + get_node_info_response_transfer_event = None + def _get_info_callback(transfer_event: dronecan.node.TransferEvent): + if transfer_event is None or transfer_event.transfer.source_node_id != APP_NODE_ID: + return # ignore other nodes if present + nonlocal get_node_info_response_transfer_event + print(transfer_event) + get_node_info_response_transfer_event = transfer_event + + node.request(req, APP_NODE_ID, _get_info_callback) + node.spin(SERVICE_TIMEOUT) + assert get_node_info_response_transfer_event is not None, ( + "GetNodeInfo response is missing. Hint: check node id, interface, and that the app is running." + ) + assert get_node_info_response_transfer_event.transfer.source_node_id == APP_NODE_ID + assert get_node_info_response_transfer_event.transfer.dest_node_id == NODE_ID_TESTER + assert get_node_info_response_transfer_event.transfer.dest_node_id == NODE_ID_TESTER + assert get_node_info_response_transfer_event.response.name.decode("utf-8").rstrip("\x00") == "com.example.libdcnode.sitl" + +if __name__ == "__main__": + raise SystemExit(pytest.main([str(Path(__file__).resolve())]))