Skip to content

Instantly share code, notes, and snippets.

@j-c-cook
Created July 11, 2025 21:22
Show Gist options
  • Save j-c-cook/020f4de33f8b6ead494dc3010a081d12 to your computer and use it in GitHub Desktop.
Save j-c-cook/020f4de33f8b6ead494dc3010a081d12 to your computer and use it in GitHub Desktop.

AgIsoStack Control Function State Callback Bug Fix Example

This project demonstrates a bug fix in the AgIsoStack library's control function state callback functionality. The issue involves incorrect address reporting in the callback when control functions change state.

Bug Description

The control function state callback was reporting invalid addresses (0xfe) instead of the actual claimed addresses when control functions came online.

Setup Virtual CAN Interface

Before running the examples, set up the vcan0 interface:

# Load the vcan kernel module
sudo modprobe vcan

# Create a virtual CAN interface
sudo ip link add dev vcan0 type vcan

# Bring the interface up
sudo ip link set up vcan0

# Verify the interface is available
ip link show vcan0

Building and Running

# Build the project (automatically downloads AgIsoStack from GitHub)
mkdir build && cd build
cmake .. && make

# Run ECU1 (with callback) in one terminal
./ecu1

# Run ECU2 in another terminal (different terminal)
./ecu2

Branch Comparison

Main Branch (Bug Present)

When using the main branch, the callback reports invalid address information:

ECU1: Control Function State Change Detected!
  Control Function: 0xaaaad8410e60
  NAME: 0x90321d00afe000c8
  Manufacturer Code: 1407
  Function Code: 29
  Identity Number: 200
  ECU Instance: 0
  Address: 0xfe          <- Invalid address
  Address Valid: No      <- Incorrect state
  Current State: Online

cf_callback_addr_fix Branch (Bug Fixed)

When using the cf_callback_addr_fix branch, the callback correctly reports the actual claimed address:

ECU1: Control Function State Change Detected!
  Control Function: 0xaaaaf1411e60
  NAME: 0x90321d00afe000c8
  Manufacturer Code: 1407
  Function Code: 29
  Identity Number: 200
  ECU Instance: 0
  Address: 0x81          <- Correct claimed address
  Address Valid: Yes     <- Correct state
  Current State: Online

Testing Different Branches

To test the fix branch, update the CMakeLists.txt:

FetchContent_Declare(
    AgIsoStack
    GIT_REPOSITORY https://github.com/Open-Agriculture/AgIsoStack-plus-plus.git
    GIT_TAG        cf_callback_addr_fix  # Use the fix branch
)

Then rebuild:

cd build
rm -rf *  # Clean build to fetch new branch
cmake .. && make
cmake_minimum_required(VERSION 3.16)
project(AgIsoStackECUProject)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Enable FetchContent
include(FetchContent)
# Fetch AgIsoStack-plus-plus from GitHub
FetchContent_Declare(
AgIsoStack
GIT_REPOSITORY https://github.com/j-c-cook/AgIsoStack-plus-plus.git
GIT_TAG main # cf_callback_addr_fix
)
# Make AgIsoStack available
FetchContent_MakeAvailable(AgIsoStack)
# Find required packages
find_package(Threads REQUIRED)
# Create ECU1 executable
add_executable(ecu1 ecu1.cpp)
target_link_libraries(ecu1
PRIVATE
isobus::Isobus
isobus::HardwareIntegration
isobus::Utility
Threads::Threads
)
# Create ECU2 executable
add_executable(ecu2 ecu2.cpp)
target_link_libraries(ecu2
PRIVATE
isobus::Isobus
isobus::HardwareIntegration
isobus::Utility
Threads::Threads
)
# Enable SocketCAN driver for Linux
if(UNIX AND NOT APPLE)
target_compile_definitions(ecu1 PRIVATE ISOBUS_SOCKETCAN_AVAILABLE)
target_compile_definitions(ecu2 PRIVATE ISOBUS_SOCKETCAN_AVAILABLE)
endif()
# Set compiler flags for debugging
if(CMAKE_BUILD_TYPE MATCHES Debug)
target_compile_options(ecu1 PRIVATE -g -O0)
target_compile_options(ecu2 PRIVATE -g -O0)
endif()
#include "isobus/hardware_integration/can_hardware_interface.hpp"
#include "isobus/hardware_integration/socket_can_interface.hpp"
#include "isobus/isobus/can_network_manager.hpp"
#include "isobus/isobus/can_partnered_control_function.hpp"
#include "isobus/isobus/can_stack_logger.hpp"
#include "isobus/isobus/can_callbacks.hpp"
#include <atomic>
#include <chrono>
#include <csignal>
#include <iostream>
#include <memory>
#include <thread>
using namespace isobus;
// Global variables
static std::atomic_bool running = { true };
// Signal handler for graceful shutdown
void signal_handler(int)
{
std::cout << "\nECU1: Received shutdown signal. Stopping..." << std::endl;
running = false;
}
// Callback function for control function state changes
void control_function_state_change_callback(std::shared_ptr<ControlFunction> controlFunction, ControlFunctionState state)
{
std::cout << "ECU1: Control Function State Change Detected!" << std::endl;
std::cout << " Control Function: " << controlFunction.get() << std::endl;
// Print NAME details if available
if (controlFunction->get_NAME().get_full_name() != 0)
{
std::cout << " NAME: 0x" << std::hex << controlFunction->get_NAME().get_full_name() << std::dec << std::endl;
std::cout << " Manufacturer Code: " << controlFunction->get_NAME().get_manufacturer_code() << std::endl;
std::cout << " Function Code: " << static_cast<int>(controlFunction->get_NAME().get_function_code()) << std::endl;
std::cout << " Identity Number: " << controlFunction->get_NAME().get_identity_number() << std::endl;
std::cout << " ECU Instance: " << static_cast<int>(controlFunction->get_NAME().get_ecu_instance()) << std::endl;
}
// Print address information
std::cout << " Address: 0x" << std::hex << static_cast<int>(controlFunction->get_address()) << std::dec << std::endl;
std::cout << " Address Valid: " << (controlFunction->get_address_valid() ? "Yes" : "No") << std::endl;
std::cout << " Current State: ";
switch (state)
{
case ControlFunctionState::Offline:
std::cout << "Offline";
break;
case ControlFunctionState::Online:
std::cout << "Online";
break;
default:
std::cout << "Unknown (" << static_cast<int>(state) << ")";
break;
}
std::cout << std::endl;
std::cout << " ------------------------" << std::endl;
}
int main()
{
std::signal(SIGINT, signal_handler);
std::signal(SIGTERM, signal_handler);
std::cout << "ECU1: Starting AgIsoStack ECU1 on vcan0..." << std::endl;
#ifndef ISOBUS_SOCKETCAN_AVAILABLE
std::cout << "ECU1: ERROR - SocketCAN not available. Make sure you're running on Linux with SocketCAN support." << std::endl;
return -1;
#endif
// Initialize the CAN hardware interface
auto canDriver = std::make_shared<SocketCANInterface>("vcan0");
CANHardwareInterface::set_number_of_can_channels(1);
CANHardwareInterface::assign_can_channel_frame_handler(0, canDriver);
if (!CANHardwareInterface::start() || !canDriver->get_is_valid())
{
std::cout << "ECU1: ERROR - Failed to start CAN hardware interface on vcan0." << std::endl;
std::cout << "ECU1: Make sure vcan0 is available with: sudo modprobe vcan && sudo ip link add dev vcan0 type vcan && sudo ip link set up vcan0" << std::endl;
return -2;
}
std::cout << "ECU1: CAN interface started successfully on " << canDriver->get_device_name() << std::endl;
// Allow some time for the interface to initialize
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// Create the NAME for this ECU
NAME ecu1Name(0);
ecu1Name.set_arbitrary_address_capable(true);
ecu1Name.set_industry_group(1);
ecu1Name.set_device_class(25); // Non-specific system
ecu1Name.set_function_code(static_cast<std::uint8_t>(NAME::Function::Engine));
ecu1Name.set_identity_number(100);
ecu1Name.set_ecu_instance(0);
ecu1Name.set_function_instance(0);
ecu1Name.set_device_class_instance(0);
ecu1Name.set_manufacturer_code(1407); // Example manufacturer code
// Create internal control function
auto ecu1ControlFunction = CANNetworkManager::CANNetwork.create_internal_control_function(ecu1Name, 0, 0x80);
if (nullptr == ecu1ControlFunction)
{
std::cout << "ECU1: ERROR - Failed to create internal control function." << std::endl;
CANHardwareInterface::stop();
return -3;
}
// Wait for address claiming to complete
std::cout << "ECU1: Waiting for address claiming..." << std::endl;
auto start_time = std::chrono::steady_clock::now();
while (!ecu1ControlFunction->get_address_valid() &&
(std::chrono::steady_clock::now() - start_time < std::chrono::seconds(5)))
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
if (!ecu1ControlFunction->get_address_valid())
{
std::cout << "ECU1: ERROR - Address claiming failed." << std::endl;
CANHardwareInterface::stop();
return -4;
}
std::cout << "ECU1: Address claiming successful! ECU1 claimed address: 0x"
<< std::hex << static_cast<int>(ecu1ControlFunction->get_address()) << std::dec << std::endl;
// Register control function state change callback
CANNetworkManager::CANNetwork.add_control_function_status_change_callback(control_function_state_change_callback);
std::cout << "ECU1: Control function state change callback registered." << std::endl;
// Create a filter to listen for ECU2 messages
const NAMEFilter ecu2Filter(NAME::NAMEParameters::FunctionCode, static_cast<std::uint8_t>(NAME::Function::VirtualTerminal));
auto ecu2Partner = CANNetworkManager::CANNetwork.create_partnered_control_function(0, { ecu2Filter });
// Main loop
std::cout << "ECU1: Entering main loop. Press Ctrl+C to stop." << std::endl;
auto last_heartbeat = std::chrono::steady_clock::now();
while (running)
{
isobus::CANNetworkManager::CANNetwork.send_request_for_address_claim(0);
// Process CAN messages and update network manager
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
std::cout << "ECU1: Shutting down..." << std::endl;
CANHardwareInterface::stop();
std::cout << "ECU1: Shutdown complete." << std::endl;
return 0;
}
#include "isobus/hardware_integration/can_hardware_interface.hpp"
#include "isobus/hardware_integration/socket_can_interface.hpp"
#include "isobus/isobus/can_network_manager.hpp"
#include "isobus/isobus/can_partnered_control_function.hpp"
#include "isobus/isobus/can_stack_logger.hpp"
#include <atomic>
#include <chrono>
#include <csignal>
#include <iostream>
#include <memory>
#include <thread>
using namespace isobus;
// Global variables
static std::atomic_bool running = { true };
// Signal handler for graceful shutdown
void signal_handler(int)
{
std::cout << "\nECU2: Received shutdown signal. Stopping..." << std::endl;
running = false;
}
int main()
{
std::signal(SIGINT, signal_handler);
std::signal(SIGTERM, signal_handler);
std::cout << "ECU2: Starting AgIsoStack ECU2 on vcan0..." << std::endl;
#ifndef ISOBUS_SOCKETCAN_AVAILABLE
std::cout << "ECU2: ERROR - SocketCAN not available. Make sure you're running on Linux with SocketCAN support." << std::endl;
return -1;
#endif
// Initialize the CAN hardware interface
auto canDriver = std::make_shared<SocketCANInterface>("vcan0");
CANHardwareInterface::set_number_of_can_channels(1);
CANHardwareInterface::assign_can_channel_frame_handler(0, canDriver);
if (!CANHardwareInterface::start() || !canDriver->get_is_valid())
{
std::cout << "ECU2: ERROR - Failed to start CAN hardware interface on vcan0." << std::endl;
std::cout << "ECU2: Make sure vcan0 is available with: sudo modprobe vcan && sudo ip link add dev vcan0 type vcan && sudo ip link set up vcan0" << std::endl;
return -2;
}
std::cout << "ECU2: CAN interface started successfully on " << canDriver->get_device_name() << std::endl;
// Allow some time for the interface to initialize
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// Create the NAME for this ECU
NAME ecu2Name(0);
ecu2Name.set_arbitrary_address_capable(true);
ecu2Name.set_industry_group(1);
ecu2Name.set_device_class(25); // Non-specific system
ecu2Name.set_function_code(static_cast<std::uint8_t>(NAME::Function::VirtualTerminal));
ecu2Name.set_identity_number(200);
ecu2Name.set_ecu_instance(0);
ecu2Name.set_function_instance(0);
ecu2Name.set_device_class_instance(0);
ecu2Name.set_manufacturer_code(1407); // Example manufacturer code
// Create internal control function
auto ecu2ControlFunction = CANNetworkManager::CANNetwork.create_internal_control_function(ecu2Name, 0, 0x81);
if (nullptr == ecu2ControlFunction)
{
std::cout << "ECU2: ERROR - Failed to create internal control function." << std::endl;
CANHardwareInterface::stop();
return -3;
}
// Wait for address claiming to complete
std::cout << "ECU2: Waiting for address claiming..." << std::endl;
auto start_time = std::chrono::steady_clock::now();
while (!ecu2ControlFunction->get_address_valid() &&
(std::chrono::steady_clock::now() - start_time < std::chrono::seconds(5)))
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
if (!ecu2ControlFunction->get_address_valid())
{
std::cout << "ECU2: ERROR - Address claiming failed." << std::endl;
CANHardwareInterface::stop();
return -4;
}
std::cout << "ECU2: Address claiming successful! ECU2 claimed address: 0x"
<< std::hex << static_cast<int>(ecu2ControlFunction->get_address()) << std::dec << std::endl;
// Create a filter to listen for ECU1 messages
const NAMEFilter ecu1Filter(NAME::NAMEParameters::FunctionCode, static_cast<std::uint8_t>(NAME::Function::Engine));
auto ecu1Partner = CANNetworkManager::CANNetwork.create_partnered_control_function(0, { ecu1Filter });
// Main loop
std::cout << "ECU2: Entering main loop. Press Ctrl+C to stop." << std::endl;
auto last_heartbeat = std::chrono::steady_clock::now();
std::uint32_t heartbeat_counter = 0;
while (running)
{
// Process CAN messages and update network manager
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
std::cout << "ECU2: Shutting down..." << std::endl;
CANHardwareInterface::stop();
std::cout << "ECU2: Shutdown complete." << std::endl;
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment