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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ CMakeFiles
*.orig
*.rej

# AI
.claude
58 changes: 57 additions & 1 deletion examples/virtual_terminal/aux_functions/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@
#include <atomic>
#include <csignal>
#include <iostream>
#include <map>
#include <memory>
#include <thread>

//! It is discouraged to use global variables, but it is done here for simplicity.
static std::shared_ptr<isobus::VirtualTerminalClient> TestVirtualTerminalClient = nullptr;
static std::atomic_bool running = { true };

// In-memory storage for auxiliary function assignments for demonstration purposes
static std::map<std::pair<std::uint64_t, std::uint16_t>, std::vector<isobus::VirtualTerminalClient::AssignedAuxiliaryFunction>> assignmentStorage;

void signal_handler(int)
{
running = false;
Expand All @@ -31,6 +35,52 @@ void handle_aux_function_input(const isobus::VirtualTerminalClient::AuxiliaryFun
std::cout << "Auxiliary function event received: (" << event.function.functionObjectID << ", " << event.function.inputObjectID << ", " << static_cast<int>(event.function.functionType) << "), value1: " << event.value1 << ", value2: " << event.value2 << std::endl;
}

// Callback to load stored auxiliary function assignments
static std::vector<isobus::VirtualTerminalClient::AssignedAuxiliaryFunction> load_assignments(
std::uint64_t deviceName,
std::uint16_t modelIdentificationCode,
void *)
{
auto key = std::make_pair(deviceName, modelIdentificationCode);
auto it = assignmentStorage.find(key);

if (it != assignmentStorage.end())
{
std::cout << "Loading " << it->second.size() << " stored assignment(s) for device "
<< std::hex << deviceName << std::dec
<< " (model ID: " << modelIdentificationCode << ")" << std::endl;
return it->second;
}

std::cout << "No stored assignments found for device "
<< std::hex << deviceName << std::dec
<< " (model ID: " << modelIdentificationCode << ")" << std::endl;
return {}; // Return empty vector if no stored assignments
}

// Callback to store auxiliary function assignments
static void store_assignments(
std::uint64_t deviceName,
std::uint16_t modelIdentificationCode,
const std::vector<isobus::VirtualTerminalClient::AssignedAuxiliaryFunction> &assignments,
void *)
{
auto key = std::make_pair(deviceName, modelIdentificationCode);
assignmentStorage[key] = assignments;

std::cout << "Stored " << assignments.size() << " assignment(s) for device "
<< std::hex << deviceName << std::dec
<< " (model ID: " << modelIdentificationCode << ")" << std::endl;

// Optionally print details of each assignment
for (const auto &assignment : assignments)
{
std::cout << " - Function ID: " << assignment.functionObjectID
<< ", Input ID: " << assignment.inputObjectID
<< ", Type: " << static_cast<int>(assignment.functionType) << std::endl;
}
}

int main()
{
std::signal(SIGINT, signal_handler);
Expand All @@ -55,7 +105,7 @@ int main()
}

isobus::CANStackLogger::set_can_stack_logger_sink(&logger);
isobus::CANStackLogger::set_log_level(isobus::CANStackLogger::LoggingLevel::Info); // Change this to Debug to see more information
isobus::CANStackLogger::set_log_level(isobus::CANStackLogger::LoggingLevel::Debug); // Change this to Debug to see more information
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log level has been changed from Info to Debug. While this may be helpful for development and testing of the new auxiliary assignment functionality, consider whether this should remain in the example code permanently, as it will generate significantly more log output for users. A comment explaining why Debug level is recommended for this example would be helpful.

Suggested change
isobus::CANStackLogger::set_log_level(isobus::CANStackLogger::LoggingLevel::Debug); // Change this to Debug to see more information
// Use Debug level here so this example shows detailed VT and auxiliary assignment activity.
// This will generate significantly more log output; for production use, consider using Info instead.
isobus::CANStackLogger::set_log_level(isobus::CANStackLogger::LoggingLevel::Debug);

Copilot uses AI. Check for mistakes.
isobus::CANHardwareInterface::set_number_of_can_channels(1);
isobus::CANHardwareInterface::assign_can_channel_frame_handler(0, canDriver);

Expand Down Expand Up @@ -102,7 +152,13 @@ int main()

TestVirtualTerminalClient = std::make_shared<isobus::VirtualTerminalClient>(TestPartnerVT, TestInternalECU);
TestVirtualTerminalClient->set_object_pool(0, testPool.data(), testPool.size(), objectPoolHash);

TestVirtualTerminalClient->get_auxiliary_function_event_dispatcher().add_listener(handle_aux_function_input);
std::cout << "Registered auxiliary function input event listener." << std::endl;

TestVirtualTerminalClient->set_auxiliary_assignment_callbacks(load_assignments, store_assignments, nullptr);
std::cout << "Registered auxiliary assignment storage callbacks (in-memory)" << std::endl;

TestVirtualTerminalClient->initialize(true);

while (running)
Expand Down
47 changes: 43 additions & 4 deletions isobus/include/isobus/isobus/isobus_virtual_terminal_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ namespace isobus
UploadObjectPool, ///< Client is uploading the object pool
SendEndOfObjectPool, ///< Client is sending the end of object pool message
WaitForEndOfObjectPoolResponse, ///< Client is waiting for the end of object pool response message
SendAuxiliaryPreferredAssignment, ///< Sending auxiliary functions preferred assignment
WaitForPreferredAssignmentResponse, ///< Waiting for Preferred Assignment OK response
Connected, ///< Client is connected to the VT server and the application layer is in control
Failed ///< Client could not connect to the VT due to an error
};
Expand All @@ -372,6 +374,27 @@ namespace isobus
AuxiliaryTypeTwoFunctionType functionType; ///< The type of function
};

/// @brief Callback to load stored auxiliary function assignments for a device
/// @param[in] deviceName The NAME of the auxiliary input device
/// @param[in] modelIdentificationCode Model identification code of the device
/// @param[in] parentPointer Generic context pointer, usually the VirtualTerminalClient
/// @returns Vector of stored assignments for this device, empty if none stored
using AuxiliaryAssignmentLoadCallback = std::vector<AssignedAuxiliaryFunction> (*)(
std::uint64_t deviceName,
std::uint16_t modelIdentificationCode,
void *parentPointer);

/// @brief Callback to store auxiliary function assignments for a device
/// @param[in] deviceName The NAME of the auxiliary input device
/// @param[in] modelIdentificationCode Model identification code of the device
/// @param[in] assignments Vector of assignments to store
/// @param[in] parentPointer Generic context pointer, usually the VirtualTerminalClient
using AuxiliaryAssignmentStoreCallback = void (*)(
std::uint64_t deviceName,
std::uint16_t modelIdentificationCode,
const std::vector<AssignedAuxiliaryFunction> &assignments,
void *parentPointer);

/// @brief The constructor for a VirtualTerminalClient
/// @param[in] partner The VT server control function
/// @param[in] clientSource The internal control function to communicate from
Expand All @@ -394,7 +417,7 @@ namespace isobus
bool get_is_initialized() const;

/// @brief Check whether the client is connected to the VT server
/// @returns true if cconnected, false otherwise
/// @returns true if connected, false otherwise
bool get_is_connected() const;

/// @brief Terminates the client and joins the worker thread if applicable
Expand Down Expand Up @@ -575,6 +598,14 @@ namespace isobus
/// @param[in] modelIdentificationCode The model identification code
void set_auxiliary_input_model_identification_code(std::uint16_t modelIdentificationCode);

/// @brief Registers callbacks for loading and storing auxiliary function assignments
/// @param[in] loadCallback Callback function to load stored assignments (required)
/// @param[in] storeCallback Callback function to store assignments (required)
/// @param[in] context User context pointer passed to callbacks (optional)
void set_auxiliary_assignment_callbacks(AuxiliaryAssignmentLoadCallback loadCallback,
AuxiliaryAssignmentStoreCallback storeCallback,
void *context = nullptr);

/// @brief Get whether the VT has enabled the learn mode for the auxiliary input
/// @returns true if the VT has enabled the learn mode for the auxiliary input
bool get_auxiliary_input_learn_mode_enabled() const;
Expand Down Expand Up @@ -1323,9 +1354,10 @@ namespace isobus
/// @brief A struct for storing information about an auxiliary input device
struct AssignedAuxiliaryInputDevice
{
const std::uint64_t name; ///< The NAME of the unit
const std::uint16_t modelIdentificationCode; ///< The model identification code
std::uint64_t name; ///< The NAME of the unit
std::uint16_t modelIdentificationCode; ///< The model identification code
Comment on lines +1357 to +1358
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The const qualifiers were removed from the name and modelIdentificationCode fields. While this change is necessary to support the timestamp updates (line 3354), consider whether these fields should still be conceptually immutable. If a device's NAME or model ID should never change after creation, consider alternative designs such as using the timestamp field as mutable while keeping these fields const, or using a key-based lookup structure.

Copilot uses AI. Check for mistakes.
std::vector<AssignedAuxiliaryFunction> functions; ///< The functions assigned to this auxiliary input device (only applicable for listeners of input)
std::uint64_t lastMaintenanceMessageTimestamp; ///< The timestamp of the last received maintenance message, in milliseconds
};

/// @brief Struct for storing the state of an auxiliary input on our device
Expand Down Expand Up @@ -1436,8 +1468,9 @@ namespace isobus
bool send_working_set_master() const;

/// @brief Send the preferred auxiliary control type 2 assignment command
/// @param[in] devices the AssignedAuxiliaryInputDevices for which to send the preferred assignment
/// @returns true if the message was sent successfully
bool send_auxiliary_functions_preferred_assignment() const;
bool send_auxiliary_functions_preferred_assignment(const std::vector<AssignedAuxiliaryInputDevice> &devices);

/// @brief Send the auxiliary control type 2 assignment reponse message
/// @param[in] functionObjectID The object ID of the function
Expand Down Expand Up @@ -1602,6 +1635,8 @@ namespace isobus
static constexpr std::uint32_t VT_STATUS_TIMEOUT_MS = 3000; ///< The max allowable time between VT status messages before its considered offline
static constexpr std::uint32_t WORKING_SET_MAINTENANCE_TIMEOUT_MS = 1000; ///< The delay between working set maintenance messages
static constexpr std::uint32_t AUXILIARY_MAINTENANCE_TIMEOUT_MS = 100; ///< The delay between auxiliary maintenance messages
static constexpr std::uint32_t AUXILIARY_INPUT_DEVICE_TIMEOUT_MS = 300; ///< The max allowable time between auxiliary input maintenance messages before the device is considered offline (ISO 11783-6)
static constexpr std::uint32_t AUXILIARY_ASSIGNMENT_RESPONSE_TIMEOUT_MS = 2000; ///< Timeout for Preferred Assignment OK response per ISO 11783

std::shared_ptr<PartneredControlFunction> partnerControlFunction; ///< The partner control function this client will send to
std::shared_ptr<InternalControlFunction> myControlFunction; ///< The internal control function the client uses to send from
Expand Down Expand Up @@ -1639,6 +1674,7 @@ namespace isobus
StateMachineState state = StateMachineState::Disconnected; ///< The current client state machine state
CurrentObjectPoolUploadState currentObjectPoolState = CurrentObjectPoolUploadState::Uninitialized; ///< The current upload state of the object pool being processed
std::uint32_t stateMachineTimestamp_ms = 0; ///< Timestamp from the last state machine update
std::uint8_t auxiliaryAssignmentRetryCount = 0; ///< Retry counter for auxiliary preferred assignment (max 2 retries = 3 attempts)
std::uint32_t lastWorkingSetMaintenanceTimestamp_ms = 0; ///< The timestamp from the last time we sent the maintenance message
std::uint32_t lastAuxiliaryMaintenanceTimestamp_ms = 0; ///< The timestamp from the last time we sent the maintenance message
std::vector<ObjectPoolDataStruct> objectPools; ///< A container to hold all object pools that have been assigned to the interface
Expand Down Expand Up @@ -1674,6 +1710,9 @@ namespace isobus
EventDispatcher<VTUserLayoutHideShowEvent> userLayoutHideShowEventDispatcher; ///< A list of all user layout hide/show callbacks
EventDispatcher<VTAudioSignalTerminationEvent> audioSignalTerminationEventDispatcher; ///< A list of all control audio signal termination callbacks
EventDispatcher<AuxiliaryFunctionEvent> auxiliaryFunctionEventDispatcher; ///< A list of all auxiliary function callbacks
AuxiliaryAssignmentLoadCallback auxiliaryAssignmentLoadCallback = nullptr; ///< Callback to load stored assignments
AuxiliaryAssignmentStoreCallback auxiliaryAssignmentStoreCallback = nullptr; ///< Callback to store assignments
void *auxiliaryAssignmentCallbackContext = nullptr; ///< User context for storage callbacks

// Object Pool info
DataChunkCallback objectPoolDataCallback = nullptr; ///< The callback to use to get pool data
Expand Down
Loading
Loading