Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions applications/nucleo-cyphal/include/CyphalApp.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class CyphalApp final : public jarnax::Loopable, public jarnax::net::ethernet::D

static constexpr uint16_t UdpPort = 9382U;
static constexpr uint32_t SubjectId = 7509U;
static constexpr UdpardPortID DiagnosticSubjectId = 8184U;
static constexpr UdpardPortID GetInfoServiceId = 430U;
static constexpr std::size_t MaxUdpPayload = 1400U;
// Cyphal/UDP binds the node-ID to the last octet of the node's source IP.
Expand Down Expand Up @@ -53,6 +54,7 @@ class CyphalApp final : public jarnax::Loopable, public jarnax::net::ethernet::D

void InitUdpard();
void PublishHeartbeat();
void PublishRecord();
void ProcessTransmitQueue();
void ServiceDispatcherInit();
void ServiceResponseHandler(struct UdpardRxRPCTransfer const& transfer);
Expand Down Expand Up @@ -96,6 +98,9 @@ class CyphalApp final : public jarnax::Loopable, public jarnax::net::ethernet::D
size_t stats_print_counter_;
UdpardTransferID heartbeat_transfer_id_;
jarnax::Ticks last_heartbeat_ticks_;
UdpardTransferID record_transfer_id_;
jarnax::Ticks last_record_ticks_;
uint32_t record_counter_;
jarnax::Ticks last_getinfo_scan_ticks_;
};

Expand Down
72 changes: 71 additions & 1 deletion applications/nucleo-cyphal/source/CyphalApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "stm32/h7xx/ethernet/Driver.hpp"
#include "strings.hpp"

#include "uavcan/diagnostic/Record_1_0.h"
#include "uavcan/node/GetInfo_1_0.h"
#include "uavcan/node/Heartbeat_1_0.h"

Expand Down Expand Up @@ -90,6 +91,14 @@ bool SameIPv4Address(HyphaIpIPv4Address_t const& a, HyphaIpIPv4Address_t const&
return std::memcmp(&a, &b, sizeof(a)) == 0;
}

unsigned long Snprint(char buffer[], size_t buffer_size, const char *format, ...) {
va_list args;
va_start(args, format);
unsigned long const length = core::vsnprint(buffer, buffer_size, format, args);
va_end(args);
return length;
}

} // namespace

namespace nucleo {
Expand Down Expand Up @@ -121,7 +130,9 @@ CyphalApp::CyphalApp(jarnax::Ticker& ticker, jarnax::BoardContext& board_context
, initialized_{false}
, filter_installed_{false}
, service_initialized_{false}
, stats_print_counter_{0U} {
, stats_print_counter_{0U}
, record_transfer_id_{0U}
, record_counter_{0U} {
for (auto& in_use : frame_in_use_) {
in_use = false;
}
Expand Down Expand Up @@ -225,6 +236,12 @@ bool CyphalApp::Execute() {
PublishHeartbeat();
}

// Publish a diagnostic Record at the same 1 s cadence so yactui shows traffic.
if ((current_ticks.value() - last_record_ticks_.value()) >= ticker_.GetTicksPerSecond().value()) {
last_record_ticks_ = current_ticks;
PublishRecord();
}

// GetInfo client scan: query the next server node in the window each 5 seconds.
if ((current_ticks.value() - last_getinfo_scan_ticks_.value()) >= 5U * ticker_.GetTicksPerSecond().value()) {
last_getinfo_scan_ticks_ = current_ticks;
Expand Down Expand Up @@ -716,6 +733,59 @@ void CyphalApp::PublishHeartbeat() {
);
}

void CyphalApp::PublishRecord() {
uavcan_diagnostic_Record_1_0 record{};
// memset-style init: the generated initialize_() inlines a deserialize_ fed a
// 1-byte buffer, which trips GCC -Warray-bounds under -Werror. All defaults are
// zero, so memset gives the identical result.
std::memset(&record, 0, sizeof(record));

record.timestamp.microsecond = 0U; // UNKNOWN (no time sync yet)
record.severity.value = (record_counter_ % 2U == 0U) //
? uavcan_diagnostic_Severity_1_0_INFO
: uavcan_diagnostic_Severity_1_0_WARNING;

char text[uavcan_diagnostic_Record_1_0_text_ARRAY_CAPACITY_];
unsigned long const length = Snprint(
text, sizeof(text), "nucleo-cyphal diagnostic #%lu uptime=%lus",
static_cast<unsigned long>(record_counter_),
static_cast<unsigned long>(NowUs(ticker_) / 1000000U)
);
std::size_t const count = length < sizeof(text) ? length : sizeof(text) - 1U;
for (std::size_t i = 0U; i < count; ++i) {
record.text.elements[i] = static_cast<uint8_t>(text[i]);
}
record.text.count = count;

++record_counter_;

uint8_t buffer[uavcan_diagnostic_Record_1_0_SERIALIZATION_BUFFER_SIZE_BYTES_]{};
size_t serialized_size = sizeof(buffer);
int8_t const err = uavcan_diagnostic_Record_1_0_serialize_(&record, buffer, &serialized_size);
if (err < 0) {
return;
}

struct UdpardPayload const payload = {
.size = serialized_size,
.data = buffer,
};

UdpardMicrosecond const now = NowUs(ticker_);
static constexpr UdpardMicrosecond RecordPeriodUs = 1000000U;

int32_t const result = udpardTxPublish(
&tx_, now + RecordPeriodUs, UdpardPriorityLow, DiagnosticSubjectId,
record_transfer_id_++, payload, this
);
jarnax::print(
"CyphalApp: record published=%d subject=%lu sev=%u text=%.*s tid=%llu\r\n",
static_cast<int>(result), static_cast<unsigned long>(DiagnosticSubjectId),
static_cast<unsigned>(record.severity.value), static_cast<int>(count),
text, static_cast<unsigned long long>(record_transfer_id_ - 1U)
);
}

UdpardMicrosecond CyphalApp::NowUs(jarnax::Ticker const& ticker) {
auto const ticks = ticker.GetTicksSinceBoot();
return static_cast<UdpardMicrosecond>(ticks.value()) * 1000LL;
Expand Down
10 changes: 10 additions & 0 deletions applications/nucleo-cyphal/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ host_unit_test(NAME cyphal-heartbeat
NO_BOARDS
)

host_unit_test(NAME cyphal-record
SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/catch2-cyphal-record.cpp
LIBRARIES
cyphal-dsdl
CATCH2
NO_CONFIGURATIONS
NO_BOARDS
)

host_unit_test(NAME cyphal-getinfo-server
SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/catch2-cyphal-getinfo-server.cpp
Expand Down
109 changes: 109 additions & 0 deletions applications/nucleo-cyphal/tests/catch2-cyphal-record.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#define CATCH_CONFIG_MAIN
#include <catch2/catch_test_macros.hpp>

#include "uavcan/diagnostic/Record_1_0.h"

#include <cstdint>
#include <cstring>

namespace {

void Zero(uint8_t* const data, size_t const size) {
std::memset(data, 0, size);
}

} // namespace

TEST_CASE("Record serialize with empty text produces the expected wire layout", "[cyphal][record]") {
uavcan_diagnostic_Record_1_0 record{};
uavcan_diagnostic_Record_1_0_initialize_(&record);
record.timestamp.microsecond = 0U;
record.severity.value = uavcan_diagnostic_Severity_1_0_INFO;
record.text.count = 0U;

uint8_t buffer[uavcan_diagnostic_Record_1_0_SERIALIZATION_BUFFER_SIZE_BYTES_]{};
size_t size = sizeof(buffer);
REQUIRE(uavcan_diagnostic_Record_1_0_serialize_(&record, buffer, &size) == NUNAVUT_SUCCESS);
// Serialized size is trimmed to the actual payload: 7 (timestamp) + 1 (severity) + 1 (text len).
REQUIRE(size == 9U);

// timestamp UNKNOWN=0 (56-bit LE zero), severity=INFO(2), text length 0
uint8_t const expected[] = {0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U, 0x00U};
REQUIRE(std::memcmp(buffer, expected, sizeof(expected)) == 0);
}

TEST_CASE("Record serialize places severity and text after the 56-bit timestamp", "[cyphal][record]") {
uavcan_diagnostic_Record_1_0 record{};
uavcan_diagnostic_Record_1_0_initialize_(&record);
record.timestamp.microsecond = 0x01020304050607ULL;
record.severity.value = uavcan_diagnostic_Severity_1_0_WARNING;
char const text[] = "hi";
record.text.count = 2U;
record.text.elements[0] = static_cast<uint8_t>(text[0]);
record.text.elements[1] = static_cast<uint8_t>(text[1]);

uint8_t buffer[uavcan_diagnostic_Record_1_0_SERIALIZATION_BUFFER_SIZE_BYTES_]{};
size_t size = sizeof(buffer);
REQUIRE(uavcan_diagnostic_Record_1_0_serialize_(&record, buffer, &size) == NUNAVUT_SUCCESS);

// timestamp 0x01020304050607 -> LE 07 06 05 04 03 02 01, severity=WARNING(4), len=2, 'h','i'
uint8_t const expected[] = {0x07U, 0x06U, 0x05U, 0x04U, 0x03U, 0x02U, 0x01U, 0x04U, 0x02U, 'h', 'i'};
REQUIRE(std::memcmp(buffer, expected, sizeof(expected)) == 0);
}

TEST_CASE("Record deserialize round-trips timestamp, severity and text", "[cyphal][record]") {
uavcan_diagnostic_Record_1_0 record{};
uavcan_diagnostic_Record_1_0_initialize_(&record);
record.timestamp.microsecond = 1234U;
record.severity.value = uavcan_diagnostic_Severity_1_0_ERROR;
char const text[] = "nucleo-cyphal";
record.text.count = sizeof(text) - 1U;
for (size_t i = 0U; i < record.text.count; ++i) {
record.text.elements[i] = static_cast<uint8_t>(text[i]);
}

uint8_t buffer[uavcan_diagnostic_Record_1_0_SERIALIZATION_BUFFER_SIZE_BYTES_]{};
size_t size = sizeof(buffer);
REQUIRE(uavcan_diagnostic_Record_1_0_serialize_(&record, buffer, &size) == NUNAVUT_SUCCESS);

uavcan_diagnostic_Record_1_0 deserialized{};
Zero(reinterpret_cast<uint8_t*>(&deserialized), sizeof(deserialized));
size_t deserialize_size = size;
REQUIRE(uavcan_diagnostic_Record_1_0_deserialize_(&deserialized, buffer, &deserialize_size) == NUNAVUT_SUCCESS);

REQUIRE(deserialized.timestamp.microsecond == 1234U);
REQUIRE(deserialized.severity.value == uavcan_diagnostic_Severity_1_0_ERROR);
REQUIRE(deserialized.text.count == sizeof(text) - 1U);
for (size_t i = 0U; i < deserialized.text.count; ++i) {
REQUIRE(deserialized.text.elements[i] == static_cast<uint8_t>(text[i]));
}
}

TEST_CASE("Record text is capped at the 112-byte array capacity", "[cyphal][record]") {
uavcan_diagnostic_Record_1_0 record{};
uavcan_diagnostic_Record_1_0_initialize_(&record);
// Saturating member of the struct; the array cannot exceed its capacity.
REQUIRE(uavcan_diagnostic_Record_1_0_text_ARRAY_CAPACITY_ == 112U);

record.text.count = 112U;
for (size_t i = 0U; i < record.text.count; ++i) {
record.text.elements[i] = static_cast<uint8_t>('A' + (i % 26U));
}

uint8_t buffer[uavcan_diagnostic_Record_1_0_SERIALIZATION_BUFFER_SIZE_BYTES_]{};
size_t size = sizeof(buffer);
REQUIRE(uavcan_diagnostic_Record_1_0_serialize_(&record, buffer, &size) == NUNAVUT_SUCCESS);

uavcan_diagnostic_Record_1_0 deserialized{};
Zero(reinterpret_cast<uint8_t*>(&deserialized), sizeof(deserialized));
size_t deserialize_size = size;
REQUIRE(uavcan_diagnostic_Record_1_0_deserialize_(&deserialized, buffer, &deserialize_size) == NUNAVUT_SUCCESS);
REQUIRE(deserialized.text.count == 112U);
REQUIRE(deserialized.text.elements[0] == static_cast<uint8_t>('A'));
REQUIRE(deserialized.text.elements[111] == 'A' + (111U % 26U));
}

TEST_CASE("Record fixed subject ID matches the registry (8184)", "[cyphal][record]") {
REQUIRE(uavcan_diagnostic_Record_1_0_HAS_FIXED_PORT_ID_ == true);
REQUIRE(uavcan_diagnostic_Record_1_0_FIXED_PORT_ID_ == 8184U);
}
Loading