diff --git a/socketcan_adapter/include/socketcan_adapter/socketcan_adapter.hpp b/socketcan_adapter/include/socketcan_adapter/socketcan_adapter.hpp index aea13b3..f005c1a 100644 --- a/socketcan_adapter/include/socketcan_adapter/socketcan_adapter.hpp +++ b/socketcan_adapter/include/socketcan_adapter/socketcan_adapter.hpp @@ -53,6 +53,38 @@ constexpr std::chrono::duration JOIN_RECEPTION_TIMEOUT_S = std::chrono::d constexpr int32_t CLOSED_SOCKET_VALUE = -1; constexpr nfds_t NUM_SOCKETS_IN_ADAPTER = 1; +/// @brief J1939 CAN ID bit layout constants +constexpr uint32_t J1939_PGN_SHIFT = 8; +constexpr uint32_t J1939_PF_SHIFT = 16; +constexpr uint32_t J1939_PF_MASK = 0xFF; +constexpr uint32_t J1939_PDU2_THRESHOLD = 0xF0; +/// @brief Masks priority (3 bits) and source address (8 bits), matches full PGN (18 bits) +constexpr uint32_t J1939_PGN_FULL_MASK = 0x03FFFF00; +/// @brief Masks priority, source address, and PDU Specific (destination addr in PDU1) +constexpr uint32_t J1939_PGN_PDU1_MASK = 0x03FF0000; + +/// @brief Convert a J1939 PGN to a CAN filter suitable for use with setFilters() +/// Handles PDU1 (PF < 0xF0) vs PDU2 (PF >= 0xF0) masking automatically: +/// - PDU2: PS is Group Extension, part of the PGN — all 18 PGN bits are matched +/// - PDU1: PS is destination address, not part of the PGN — only 10 bits are matched +/// @param pgn The PGN value (18-bit, e.g. 0xFEF1) +/// @return can_filter with appropriate can_id and can_mask set +static inline struct can_filter j1939PgnToFilter(const uint32_t pgn) +{ + const uint8_t pf = static_cast((pgn >> J1939_PGN_SHIFT) & J1939_PF_MASK); + + struct can_filter filter{}; + filter.can_id = (pgn << J1939_PGN_SHIFT) | CAN_EFF_FLAG; + + if (J1939_PDU2_THRESHOLD <= pf) { + filter.can_mask = J1939_PGN_FULL_MASK | CAN_EFF_FLAG; + } else { + filter.can_mask = J1939_PGN_PDU1_MASK | CAN_EFF_FLAG; + } + + return filter; +} + /// @class polymath::socketcan::SocketcanAdapter /// @brief Creates and manages a socketcan instance and simplifies the interface. /// Generally does not throw, but returns booleans to tell you success diff --git a/socketcan_adapter/test/can_frame_test.cpp b/socketcan_adapter/test/can_frame_test.cpp index a630d6e..bf91d10 100644 --- a/socketcan_adapter/test/can_frame_test.cpp +++ b/socketcan_adapter/test/can_frame_test.cpp @@ -269,3 +269,38 @@ TEST_CASE("Extended constructor initializes timestamps correctly", "[CanFrame]") auto expected_bus_time = std::chrono::system_clock::time_point(std::chrono::microseconds(bus_timestamp)); REQUIRE(bus_time == expected_bus_time); } + +TEST_CASE("j1939PgnToFilter PDU2 matches full PGN", "[J1939]") +{ + // PGN 0xFEF1 (Engine Coolant Temperature) — PF=0xFE >= 0xF0, so PDU2 + const auto filter = polymath::socketcan::j1939PgnToFilter(0xFEF1); + + // can_id should be PGN shifted left 8 bits with EFF flag + REQUIRE(filter.can_id == ((0xFEF1U << 8) | CAN_EFF_FLAG)); + // PDU2: full 18-bit PGN mask + EFF flag + REQUIRE(filter.can_mask == (polymath::socketcan::J1939_PGN_FULL_MASK | CAN_EFF_FLAG)); +} + +TEST_CASE("j1939PgnToFilter PDU1 masks out destination address", "[J1939]") +{ + // PGN 0xEA00 (Request PGN) — PF=0xEA < 0xF0, so PDU1 + const auto filter = polymath::socketcan::j1939PgnToFilter(0xEA00); + + REQUIRE(filter.can_id == ((0xEA00U << 8) | CAN_EFF_FLAG)); + // PDU1: only upper 10 bits of PGN (EDP+DP+PF), PS bits unmasked + REQUIRE(filter.can_mask == (polymath::socketcan::J1939_PGN_PDU1_MASK | CAN_EFF_FLAG)); +} + +TEST_CASE("j1939PgnToFilter PDU1 boundary at 0xEF", "[J1939]") +{ + // PF=0xEF is the last PDU1 value (< 0xF0) + const auto filter = polymath::socketcan::j1939PgnToFilter(0xEF00); + REQUIRE(filter.can_mask == (polymath::socketcan::J1939_PGN_PDU1_MASK | CAN_EFF_FLAG)); +} + +TEST_CASE("j1939PgnToFilter PDU2 boundary at 0xF0", "[J1939]") +{ + // PF=0xF0 is the first PDU2 value (>= 0xF0) + const auto filter = polymath::socketcan::j1939PgnToFilter(0xF000); + REQUIRE(filter.can_mask == (polymath::socketcan::J1939_PGN_FULL_MASK | CAN_EFF_FLAG)); +}