Skip to content
Open
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
16 changes: 16 additions & 0 deletions mvec/mvec_lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,22 @@ if(BUILD_TESTING)
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)

add_executable(relay_socketcan_tests
test/mvec_relay_socketcan.cpp
)
target_link_libraries(relay_socketcan_tests
PRIVATE ${PROJECT_NAME} Catch2::Catch2WithMain
)
ament_add_test(
relay_socketcan_tests
GENERATE_RESULT_FOR_RETURN_CODE_ZERO
COMMAND "$<TARGET_FILE:relay_socketcan_tests>"
-r junit -s
-o test_results/${PROJECT_NAME}/relay_socketcan_tests_output.xml
ENV CATCH_CONFIG_CONSOLE_WIDTH=120
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)

# Only build hardware tests if CAN_AVAILABLE is set in the environment
if(DEFINED ENV{CAN_AVAILABLE})
add_executable(socketcan_hardware_tests
Expand Down
10 changes: 5 additions & 5 deletions mvec/mvec_lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A comprehensive C++ library for controlling and monitoring MVEC (multiplexed Veh
The foundational class providing J1939 message parsing, relay command generation, and status message handling. Supports all MVEC protocol operations including relay control, population queries, and diagnostic monitoring.

### MvecRelaySocketcan
High-level asynchronous interface built on top of MvecRelay using SocketCAN for Linux CAN communication. Features thread-safe promise/future patterns for non-blocking operations and automatic response handling.
High-level asynchronous interface built on top of MvecRelay using SocketCAN. Each request method (query, command, population) returns a `std::future` for the response. If a new request of the same type is made while one is in-flight, the previous promise is abandoned and the caller's future throws `broken_promise`. Callers use `wait_for()` to handle response timeouts. Per-type mutexes ensure thread safety between `parse()` and request methods.

## Quick Start

Expand Down Expand Up @@ -44,15 +44,15 @@ cd build/mvec_lib && ./mvec_socketcan_hardware.cpp
#include "socketcan_adapter/socketcan_adapter.hpp"

// Create SocketCAN adapter
auto adapter = std::make_shared<socketcan::SocketcanAdapter>("can0");
adapter->open();
auto adapter = std::make_shared<polymath::socketcan::SocketcanAdapter>("can0");
adapter->openSocket();

// Create MVEC controller
polymath::sygnal::MvecRelaySocketcan controller(adapter);

// Set up callback for incoming messages
adapter->setReceptionCallback([&controller](const socketcan::CanFrame& frame) {
controller.parse(frame);
adapter->setOnReceiveCallback([&controller](std::unique_ptr<const polymath::socketcan::CanFrame> frame) {
controller.parse(*frame);
});
```

Expand Down
44 changes: 23 additions & 21 deletions mvec/mvec_lib/include/mvec_lib/mvec_relay_socketcan.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,22 @@
#include <future>
#include <memory>
#include <mutex>
#include <queue>
#include <optional>

#include "mvec_lib/mvec_relay.hpp"
#include "socketcan_adapter/socketcan_adapter.hpp"

namespace polymath::sygnal
{

/// @brief MVEC relay controller with async SocketCAN communication
/// Provides high-level interface for MVEC relay control with thread-safe promise/future pattern
/// @brief MVEC relay controller with async SocketCAN communication.
/// Each request method (query/command/population) abandons any in-flight request of the same type,
/// sends a new CAN frame, and returns a future for the response.
/// If a previous request was still pending, its promise is destroyed and the caller's future
/// throws broken_promise on get(). Callers use wait_for() to handle response timeouts.
class MvecRelaySocketcan
{
public:
/// @brief Constructor
/// @param socketcan_adapter Shared pointer to socketcan adapter for CAN communication
explicit MvecRelaySocketcan(std::shared_ptr<socketcan::SocketcanAdapter> socketcan_adapter);

Expand All @@ -49,44 +51,44 @@ class MvecRelaySocketcan
void clear_relay();

/// @brief Query current relay states asynchronously
/// @return Future that will contain relay query reply
/// Abandons any in-flight query (caller's future throws broken_promise).
/// @return Future containing relay query reply. Use wait_for() to handle timeouts.
std::future<MvecRelayQueryReply> get_relay_state();

/// @brief Send relay command and wait for confirmation
/// @return Future that will contain command reply
/// @brief Send relay command and get confirmation asynchronously
/// Abandons any in-flight command (caller's future throws broken_promise).
/// @return Future containing command reply. Use wait_for() to handle timeouts.
std::future<MvecRelayCommandReply> send_relay_command();

/// @brief Query device population (which relays/fuses are installed)
/// @return Future that will contain population reply
/// Abandons any in-flight query (caller's future throws broken_promise).
/// @return Future containing population reply. Use wait_for() to handle timeouts.
std::future<MvecPopulationReply> get_relay_population();

/// @brief Get last received fuse status message
/// @return Optional containing fuse status if valid data available
const std::optional<MvecFuseStatusMessage> get_last_fuse_status();
std::optional<MvecFuseStatusMessage> get_last_fuse_status() const;

/// @brief Get last received relay status message
/// @return Optional containing relay status if valid data available
const std::optional<MvecRelayStatusMessage> get_last_relay_status();
std::optional<MvecRelayStatusMessage> get_last_relay_status() const;

/// @brief Get last received error status message
/// @return Optional containing error status if valid data available
const std::optional<MvecErrorStatusMessage> get_last_error_status();
std::optional<MvecErrorStatusMessage> get_last_error_status() const;

private:
/// @brief SocketCAN adapter for CAN communication
std::shared_ptr<socketcan::SocketcanAdapter> socketcan_adapter_;
/// @brief Core MVEC relay implementation
MvecRelay relay_impl_;

/// @brief Queue of promises waiting for relay query responses
std::queue<std::promise<MvecRelayQueryReply>> query_reply_promises_;
/// @brief Queue of promises waiting for relay command responses
std::queue<std::promise<MvecRelayCommandReply>> command_reply_promises_;
/// @brief Queue of promises waiting for population query responses
std::queue<std::promise<MvecPopulationReply>> population_reply_promises_;
std::optional<std::promise<MvecRelayQueryReply>> query_reply_promise_;
std::mutex query_mutex_;

/// @brief Mutex protecting promise queues for thread safety
std::mutex promises_mutex_;
std::optional<std::promise<MvecRelayCommandReply>> command_reply_promise_;
std::mutex command_mutex_;

std::optional<std::promise<MvecPopulationReply>> population_reply_promise_;
std::mutex population_mutex_;
};

} // namespace polymath::sygnal
Expand Down
119 changes: 43 additions & 76 deletions mvec/mvec_lib/src/mvec_relay_socketcan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#include "mvec_lib/mvec_relay_socketcan.hpp"

#include <memory>
#include <mutex>
#include <utility>

Expand All @@ -27,39 +28,35 @@ MvecRelaySocketcan::MvecRelaySocketcan(std::shared_ptr<socketcan::SocketcanAdapt

MvecMessageType MvecRelaySocketcan::parse(const socketcan::CanFrame & frame)
{
MvecMessageType message_type = relay_impl_.parseMessage(frame);

// Check if we received expected response types and fulfill promises
std::lock_guard<std::mutex> lock(promises_mutex_);
const MvecMessageType message_type = relay_impl_.parseMessage(frame);

// Check if we received an expected response type and fulfill the waiting promise
switch (message_type) {
case MvecMessageType::RELAY_QUERY_RESPONSE: {
const auto & reply = relay_impl_.get_last_relay_query_reply();
if (reply.is_valid() && !query_reply_promises_.empty()) {
// Get the oldest waiting promise
auto promise = std::move(query_reply_promises_.front());
query_reply_promises_.pop();

// Fulfill the promise
promise.set_value(reply);
std::lock_guard<std::mutex> lock(query_mutex_);
if (reply.is_valid() && query_reply_promise_.has_value()) {
// Fulfill the promise and clear the slot
query_reply_promise_->set_value(reply);
query_reply_promise_.reset();
}
break;
}
case MvecMessageType::RELAY_COMMAND_RESPONSE: {
const auto & reply = relay_impl_.get_last_relay_command_reply();
if (reply.is_valid() && !command_reply_promises_.empty()) {
auto promise = std::move(command_reply_promises_.front());
command_reply_promises_.pop();
promise.set_value(reply);
std::lock_guard<std::mutex> lock(command_mutex_);
if (reply.is_valid() && command_reply_promise_.has_value()) {
command_reply_promise_->set_value(reply);
command_reply_promise_.reset();
}
break;
}
case MvecMessageType::POPULATION_RESPONSE: {
const auto & reply = relay_impl_.get_last_population_reply();
if (reply.is_valid() && !population_reply_promises_.empty()) {
auto promise = std::move(population_reply_promises_.front());
population_reply_promises_.pop();
promise.set_value(reply);
std::lock_guard<std::mutex> lock(population_mutex_);
if (reply.is_valid() && population_reply_promise_.has_value()) {
population_reply_promise_->set_value(reply);
population_reply_promise_.reset();
}
break;
}
Expand All @@ -84,110 +81,80 @@ void MvecRelaySocketcan::clear_relay()

std::future<MvecRelayQueryReply> MvecRelaySocketcan::get_relay_state()
{
// Get the query message from the relay implementation
/// TODO: (zeerek) Set invalid for received message
auto query_frame = relay_impl_.getRelayQueryMessage();
std::lock_guard<std::mutex> lock(query_mutex_);

// Abandon any in-flight query, caller's future becomes broken_promise if still waiting
query_reply_promise_.reset();

// Create a new promise and get its future
std::promise<MvecRelayQueryReply> promise;
auto future = promise.get_future();

// Add promise to the queue with thread safety
{
std::lock_guard<std::mutex> lock(promises_mutex_);
query_reply_promises_.push(std::move(promise));
}
query_reply_promise_.emplace(std::move(promise));

// Transmit the query message via socketcan adapter
socketcan_adapter_->send(query_frame);

socketcan_adapter_->send(relay_impl_.getRelayQueryMessage());
return future;
}

std::future<MvecRelayCommandReply> MvecRelaySocketcan::send_relay_command()
{
// Get the command message from the relay implementation
/// TODO: (zeerek) Set invalid for received message
auto command_frame = relay_impl_.getRelayCommandMessage();
std::lock_guard<std::mutex> lock(command_mutex_);

// Abandon any in-flight command, caller's future becomes broken_promise if still waiting
command_reply_promise_.reset();

// Create a new promise and get its future
std::promise<MvecRelayCommandReply> promise;
auto future = promise.get_future();

// Add promise to the queue with thread safety
{
std::lock_guard<std::mutex> lock(promises_mutex_);
command_reply_promises_.push(std::move(promise));
}
command_reply_promise_.emplace(std::move(promise));

// Transmit the command message via socketcan adapter
socketcan_adapter_->send(command_frame);

socketcan_adapter_->send(relay_impl_.getRelayCommandMessage());
return future;
}

std::future<MvecPopulationReply> MvecRelaySocketcan::get_relay_population()
{
// Get the population query message from the relay implementation
/// TODO: (zeerek) Set invalid for received message
auto population_frame = relay_impl_.getPopulationQueryMessage();
std::lock_guard<std::mutex> lock(population_mutex_);

// Abandon any in-flight query, caller's future becomes broken_promise if still waiting
population_reply_promise_.reset();

// Create a new promise and get its future
std::promise<MvecPopulationReply> promise;
auto future = promise.get_future();

// Add promise to the queue with thread safety
{
std::lock_guard<std::mutex> lock(promises_mutex_);
population_reply_promises_.push(std::move(promise));
}
population_reply_promise_.emplace(std::move(promise));

// Transmit the population query message via socketcan adapter
socketcan_adapter_->send(population_frame);

socketcan_adapter_->send(relay_impl_.getPopulationQueryMessage());
return future;
}

const std::optional<MvecFuseStatusMessage> MvecRelaySocketcan::get_last_fuse_status()
std::optional<MvecFuseStatusMessage> MvecRelaySocketcan::get_last_fuse_status() const
{
static std::optional<MvecFuseStatusMessage> result;

const auto & fuse_status = relay_impl_.get_fuse_status_message();
if (fuse_status.is_valid()) {
result = fuse_status;
} else {
result = std::nullopt;
return fuse_status;
}

return result;
return std::nullopt;
}

const std::optional<MvecRelayStatusMessage> MvecRelaySocketcan::get_last_relay_status()
std::optional<MvecRelayStatusMessage> MvecRelaySocketcan::get_last_relay_status() const
{
static std::optional<MvecRelayStatusMessage> result;

const auto & relay_status = relay_impl_.get_relay_status_message();
if (relay_status.is_valid()) {
result = relay_status;
} else {
result = std::nullopt;
return relay_status;
}

return result;
return std::nullopt;
}

const std::optional<MvecErrorStatusMessage> MvecRelaySocketcan::get_last_error_status()
std::optional<MvecErrorStatusMessage> MvecRelaySocketcan::get_last_error_status() const
{
static std::optional<MvecErrorStatusMessage> result;

const auto & error_status = relay_impl_.get_error_status_message();
if (error_status.is_valid()) {
result = error_status;
} else {
result = std::nullopt;
return error_status;
}

return result;
return std::nullopt;
}

} // namespace polymath::sygnal
Loading
Loading