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
6 changes: 6 additions & 0 deletions examples/task_controller_server/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ class MyTCServer : public isobus::TaskControllerServer
// This callback lets you know when a client sends a process data acknowledge (PDACK) message to you
}

void on_client_version_received(std::shared_ptr<isobus::ControlFunction> clientControlFunction, std::uint8_t version) override
{
// This callback is called when a client's Task Controller version is received
// You can use this to know what version the connected implement supports
}

bool on_value_command(std::shared_ptr<isobus::ControlFunction>, std::uint16_t, std::uint16_t, std::int32_t, std::uint8_t &) override
{
return true;
Expand Down
21 changes: 21 additions & 0 deletions isobus/include/isobus/isobus/isobus_task_controller_server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,16 @@ namespace isobus
/// @param[in] processDataCommand The process data command that was acknowledged.
virtual void on_process_data_acknowledge(std::shared_ptr<ControlFunction> clientControlFunction, std::uint16_t dataDescriptionIndex, std::uint16_t elementNumber, std::uint8_t errorCodesFromClient, ProcessDataCommands processDataCommand) = 0;

/// @brief This function will be called by the server when a client's version information is received.
/// This happens when the client sends its version in response to a RequestVersion command, or
/// when the client spontaneously sends its version information.
/// @note Per the standard, version exchange is OPTIONAL in V3 (SecondEditionDraft) but the callback
/// will fire whenever version info is available. In V4+ (SecondPublishedEdition), version exchange
/// is more standardized and this callback will fire more predictably.
/// @param[in] clientControlFunction The control function whose version was received.
/// @param[in] version The Task Controller version reported by the client (see TaskControllerVersion enum).
virtual void on_client_version_received(std::shared_ptr<ControlFunction> clientControlFunction, std::uint8_t version) = 0;

/// @brief This function will be called by the server when a client sends a value command to the TC.
/// You should implement this function to do whatever you want to do when a client sends a value command.
/// This could be anything from setting a value in your program, to sending a command to a connected implement.
Expand Down Expand Up @@ -344,6 +354,17 @@ namespace isobus
/// @returns Whether a task is currently active or not.
bool get_task_totals_active() const;

/// @brief Returns the version reported by a specific client.
/// @param[in] clientControlFunction The control function to get the version for.
/// @returns The Task Controller version reported by the client, or 0xFF if no version has been received yet.
std::uint8_t get_client_version(std::shared_ptr<ControlFunction> clientControlFunction) const;

/// @brief Requests version information from a client.
/// This sends a RequestVersion command to learn the client's Task Controller version.
/// @param[in] clientControlFunction The control function to request version information from.
/// @returns true if the message was sent, otherwise false.
bool request_client_version(std::shared_ptr<ControlFunction> clientControlFunction) const;

/// @brief Returns the language command interface used to communicate with the client which language/units are in use.
/// The language command is very important for the TC to function correctly, so it is recommended that you call this
/// function and configure the language command interface before calling initialize().
Expand Down
33 changes: 33 additions & 0 deletions isobus/src/isobus_task_controller_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,12 @@ namespace isobus
// We can store the reported version to use the proper DDOP parsing approach later on.
LOG_DEBUG("[TC Server]: Client reports that its version is %u", version);
get_active_client(rxMessage.get_source_control_function())->reportedVersion = version;

// Notify the application that we received version information
LOG_INFO("[TC Server]: Client 0x%02X reported Task Controller version %u",
rxMessage.get_source_control_function()->get_address(),
version);
on_client_version_received(rxMessage.get_source_control_function(), version);
}
}
break;
Expand Down Expand Up @@ -699,6 +705,11 @@ namespace isobus
if (nullptr == get_active_client(rxMessage.get_source_control_function()))
{
activeClients.push_back(std::make_shared<ActiveClient>(rxMessage.get_source_control_function()));
LOG_INFO("[TC Server]: New client 0x%02X detected via WorkingSetMaster. Requesting version.",
rxMessage.get_source_control_function()->get_address());
// Proactively request version information from the new client
send_generic_process_data_default_payload(static_cast<std::uint8_t>(TechnicalDataCommandParameters::RequestVersion),
rxMessage.get_source_control_function());
}
}
else
Expand Down Expand Up @@ -814,6 +825,28 @@ namespace isobus
payload.size());
}

std::uint8_t TaskControllerServer::get_client_version(std::shared_ptr<ControlFunction> clientControlFunction) const
{
auto activeClient = get_active_client(clientControlFunction);
if (nullptr != activeClient)
{
return activeClient->reportedVersion;
}
return 0xFF; // Return unknown if client not found
}

bool TaskControllerServer::request_client_version(std::shared_ptr<ControlFunction> clientControlFunction) const
{
if (nullptr == clientControlFunction)
{
return false;
}

LOG_INFO("[TC Server]: Requesting version from client 0x%02X", clientControlFunction->get_address());
return send_generic_process_data_default_payload(static_cast<std::uint8_t>(TechnicalDataCommandParameters::RequestVersion),
clientControlFunction);
}

std::shared_ptr<TaskControllerServer::ActiveClient> TaskControllerServer::get_active_client(std::shared_ptr<ControlFunction> clientControlFunction) const
{
for (const auto &activeClient : activeClients)
Expand Down
129 changes: 129 additions & 0 deletions test/tc_server_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ class DerivedTcServer : public TaskControllerServer
{
}

void on_client_version_received(std::shared_ptr<ControlFunction> clientCF, std::uint8_t version) override
{
lastClientVersion = version;
lastClientVersionReceived = true;
}

bool on_value_command(std::shared_ptr<ControlFunction>, std::uint16_t, std::uint16_t, std::int32_t, std::uint8_t &) override
{
return true;
Expand Down Expand Up @@ -285,6 +291,8 @@ class DerivedTcServer : public TaskControllerServer
std::uint8_t identifyTC = 0xFF;
bool failActivations = false;
bool enoughMemory = true;
std::uint8_t lastClientVersion = 0xFF;
bool lastClientVersionReceived = false;
};

void isNack(const CANMessageFrame &frame)
Expand Down Expand Up @@ -497,6 +505,19 @@ TEST_F(TaskControllerServerTest, MessageEncoding)
CANNetworkManager::CANNetwork.update();
server.update();

// After working set master, server should now proactively request version from client
// Skip the automatic version request message
EXPECT_TRUE(readFrameFilterStatus(testPlugin, testFrame, time_source));
EXPECT_EQ(testFrame.identifier, 0x14CB8887);
EXPECT_EQ(0x00, testFrame.data[0]); // RequestVersion command
EXPECT_EQ(0xFF, testFrame.data[1]);
EXPECT_EQ(0xFF, testFrame.data[2]);
EXPECT_EQ(0xFF, testFrame.data[3]);
EXPECT_EQ(0xFF, testFrame.data[4]);
EXPECT_EQ(0xFF, testFrame.data[5]);
EXPECT_EQ(0xFF, testFrame.data[6]);
EXPECT_EQ(0xFF, testFrame.data[7]);

// Request structure label
CANNetworkManager::CANNetwork.process_receive_can_message_frame(test_helpers::create_message_frame(5,
0xCB00,
Expand Down Expand Up @@ -1189,6 +1210,114 @@ TEST_F(TaskControllerServerTest, MessageEncoding)
CANHardwareInterface::stop();
}

TEST_F(TaskControllerServerTest, ClientVersionTracking)
{
VirtualCANPlugin testPlugin;
testPlugin.open();

CANHardwareInterface::set_number_of_can_channels(1);
CANHardwareInterface::assign_can_channel_frame_handler(0, std::make_shared<VirtualCANPlugin>());
CANHardwareInterface::start(false);

// Use different addresses to avoid conflicts with previous tests
auto internalECU = test_helpers::claim_internal_control_function(0x90, 0, time_source);
auto partnerClient = test_helpers::force_claim_partnered_control_function(0x91, 0);

DerivedTcServer server(internalECU,
4,
255,
16,
TaskControllerOptions()
.with_documentation()
.with_implement_section_control()
.with_tc_geo_with_position_based_control());
server.initialize();

testPlugin.clear_queue();

// First, send a WorkingSetMaster to register the client
CANNetworkManager::CANNetwork.process_receive_can_message_frame(test_helpers::create_message_frame_broadcast(6,
0xFE0D,
partnerClient,
{
0x01,
0xFF,
0xFF,
0xFF,
0xFF,
0xFF,
0xFF,
0xFF,
}));
CANNetworkManager::CANNetwork.update();
server.update();

// Skip the automatic version request that gets sent
time_source.update_for_ms(5);
CANMessageFrame testFrame = {};
testPlugin.read_frame(testFrame);

// Initially, client version should be 0 (default before receiving version info)
EXPECT_EQ(0x00, server.get_client_version(partnerClient));
EXPECT_FALSE(server.lastClientVersionReceived);

// Simulate client sending version response (ParameterVersion command)
// This happens after server requests version from client
CANNetworkManager::CANNetwork.process_receive_can_message_frame(test_helpers::create_message_frame(5,
0xCB00,
internalECU,
partnerClient,
{
0x10, // ParameterVersion command
0x04, // Version 4 (SecondPublishedEdition)
0xFF, // Boot time (not available)
0x1F, // Options byte 1
0x00, // Options byte 2
0x01, // Booms
0x20, // Sections
0x10, // Channels
}));
CANNetworkManager::CANNetwork.update();
server.update();

// Verify the callback was triggered and version was stored
EXPECT_TRUE(server.lastClientVersionReceived);
EXPECT_EQ(0x04, server.lastClientVersion);
EXPECT_EQ(0x04, server.get_client_version(partnerClient));

// Test that we can also request version from client proactively
server.lastClientVersionReceived = false;
server.lastClientVersion = 0xFF;

bool requestSent = server.request_client_version(partnerClient);
EXPECT_TRUE(requestSent);

// Simulate client responding with version 3 (SecondEditionDraft)
CANNetworkManager::CANNetwork.process_receive_can_message_frame(test_helpers::create_message_frame(5,
0xCB00,
internalECU,
partnerClient,
{
0x10, // ParameterVersion command
0x03, // Version 3 (SecondEditionDraft)
0xFF,
0x1F,
0x00,
0x01,
0x20,
0x10,
}));
CANNetworkManager::CANNetwork.update();
server.update();

// Verify updated version
EXPECT_TRUE(server.lastClientVersionReceived);
EXPECT_EQ(0x03, server.lastClientVersion);
EXPECT_EQ(0x03, server.get_client_version(partnerClient));

CANHardwareInterface::stop();
}

TEST_F(TaskControllerServerTest, DDOPHelper_SeederExample)
{
DeviceDescriptorObjectPool ddop(3);
Expand Down
Loading