Using the PacketManager

In this tutorial, you will learn how to use the Hi-CAN PacketManager class to handle managing parameter receptions and scheduled transmissions.

Requirements

Before starting this tutorial, follow the Setup here.

Receive Callbacks

Note

The program written in this section of the tutorial can be run with:

nix run .#pkgs.examples.hi-can.packet-manager-rx

As before, we will be starting off by creating a RawCanInterface instance to interact with the bus, and a function to print out frame data, which in this case is just the renamed rx_callback() function from the previous tutorial. However, we will also be instantiating a PacketManager and calling PacketManager::handle() in a main loop. Since this code now includes an infinite main loop, we also need to add a SIGINT handler so we can use Ctrl+C to shut the program down cleanly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <signal.h>
#include <unistd.h>

#include <chrono>
#include <hi_can_raw.hpp>
#include <iostream>
#include <string>
#include <thread>
#include <vector>

using namespace hi_can;
using namespace std::chrono_literals;
using std::cout, std::cin, std::endl;

void signal_handler(int signal);
void print_frame_data(const Packet&);

int main(int argc, const char** argv)
{
    std::string interface_id = "vcan0";
    if (argc > 1)
        interface_id = argv[1];

    // handle CTRL+C cleanly
    struct sigaction sigint_handler;
    sigint_handler.sa_handler = signal_handler;
    sigemptyset(&sigint_handler.sa_mask);
    sigint_handler.sa_flags = 0;
    sigaction(SIGINT, &sigint_handler, NULL);

    try
    {
        RawCanInterface can_interface(interface_id);
        cout << "Opened CAN interface: " << interface_id << endl;

        PacketManager packet_manager(can_interface);

        bool running = true;
        while (running)
        {
            packet_manager.handle();
            std::this_thread::sleep_for(10ms); // (1)
        }
    }
    catch (const std::exception& e)
    {
        cout << "Error: " << e.what() << endl;
        return 1;
    }

    return 0;
}

void signal_handler(int signal)
{
    cout << "SIGINT caught, shutting down..." << endl;
    running = false;
}

void print_frame_data(const Packet& frame)
{
    // pull out the data we want
    const auto& address = frame.get_address();
    const auto& data = frame.get_data();

    // print out the address info
    cout << std::format("Address: {:#10x}\tExtended: {}\tRTR: {}\tError: {}\t",
                        address.address,
                        address.is_extended,
                        address.is_rtr,
                        address.is_error)
         << endl;

    // and data
    cout << "Data length: " << data.size() << " bytes" << endl;
    if (data.size() > 0 && !address.is_rtr)
    {
        cout << "Data: ";
        for (const auto byte : data)
            cout << std::format("{:#04x} ", byte);
        cout << endl;
    }
}
  1. The using namespace std::chrono_literals; line allows us to define std::chrono::steady_clock::duration intervals as numbers with unit suffixes, like 10ms or 5s.

Important

You may note on line 31 that there is a delay in the main loop. Since we’re calling PacketManager::handle() in non-blocking mode, without a delay the loop will run as fast as it can and max out the CPU core it’s running on. Alternatively, if we did call it in blocking mode, it would idle until a frame is received, but also be unable to run scheduled transmissions except directly after receiving a frame.

With the setup complete, the next step is to configure it to actually do something. We can configure the PacketManager to call specific functions when frame IDs matching a filter are received with PacketManager::set_callback(). Obviously, to do that, we need a filter:

// ...
        addressing::filter_t filter_1{
            .address = 0x12345678,
            .mask = 0xFFFF0000,
        };
// ...

This particular filter will receive any frames with an ID starting with 0x1234. Even though the full address it’s set to match is 0x12345678, since the last 4 digits aren’t included in the mask, they will be ignored in the comparison. The other component we need is a valid callback configuration, then we can configure the PacketManager:

// ...
        PacketManager::callback_config_t config_1{
            .data_callback = rx_callback_1,
        };
        packet_manager.set_callback(filter_1, config_1);
// ...

This particular configuration will just call the rx_callback_1 function whenever it receives data. Repeating the setup with another filter gives the full program:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#include <signal.h>
#include <unistd.h>

#include <chrono>
#include <hi_can_raw.hpp>
#include <iostream>
#include <string>
#include <thread>
#include <vector>

using namespace hi_can;
using namespace std::chrono_literals;
using std::cout, std::cin, std::endl;

void signal_handler(int signal);
void rx_callback_1(const Packet&);
void rx_callback_2(const Packet&);
void print_frame_data(const Packet&);
void timeout_callback();
void recovery_callback(const Packet&);

static bool running = true;

int main(int argc, const char** argv)
{
    std::string interface_id = "vcan0";
    if (argc > 1)
        interface_id = argv[1];

    // handle CTRL+C cleanly: https://stackoverflow.com/questions/1641182/how-can-i-catch-a-ctrl-c-event
    struct sigaction sigint_handler;
    sigint_handler.sa_handler = signal_handler;
    sigemptyset(&sigint_handler.sa_mask);
    sigint_handler.sa_flags = 0;
    sigaction(SIGINT, &sigint_handler, NULL);

    try
    {
        RawCanInterface can_interface(interface_id);
        cout << "Opened CAN interface: " << interface_id << endl;

        PacketManager packet_manager(can_interface);

        addressing::filter_t filter_1{
            .address = 0x12345678,
            .mask = 0xFFFF0000,
        };
        PacketManager::callback_config_t config_1{
            .data_callback = rx_callback_1,
        };
        packet_manager.set_callback(filter_1, config_1);

        addressing::filter_t filter_2{
            .address = 0x10005678,
            .mask = 0xFFFF0000,
        };
        PacketManager::callback_config_t config_2{
            .data_callback = rx_callback_2,
            .timeout_callback = timeout_callback,
            .timeout_recovery_callback = recovery_callback,
            .timeout = 1s,
        };
        packet_manager.set_callback(filter_2, config_2);

        while (running)
        {
            packet_manager.handle();
            std::this_thread::sleep_for(10ms);
        }
    }
    catch (const std::exception& e)
    {
        cout << "Error: " << e.what() << endl;
        return 1;
    }

    return 0;
}

void signal_handler(int signal)
{
    (void)signal;  // silence unused variable warning
    cout << endl
         << "SIGINT caught, shutting down..." << endl;
    running = false;
}

void rx_callback_1(const Packet& frame)
{
    cout << "From RX Callback 1!" << endl;
    print_frame_data(frame);
}

void rx_callback_2(const Packet& frame)
{
    cout << "From RX Callback 2!" << endl;
    print_frame_data(frame);
}

void print_frame_data(const Packet& frame)
{
    // pull out the data we want
    const auto& address = frame.get_address();
    const auto& data = frame.get_data();

    // print out the address info
    cout << std::format("Address: {:#10x}\tExtended: {}\tRTR: {}\tError: {}\t",
                        address.address,
                        address.is_extended,
                        address.is_rtr,
                        address.is_error)
         << endl;

    // and data
    cout << "Data length: " << data.size() << " bytes" << endl;
    if (data.size() > 0 && !address.is_rtr)
    {
        cout << "Data: ";
        for (const auto byte : data)
            cout << std::format("{:#04x} ", byte);
        cout << endl;
    }
}

void timeout_callback()
{
    cout << "Timeout callback called!" << endl;
}

void recovery_callback(const Packet& frame)
{
    (void)frame;  // silence unused warning
    cout << endl
         << "Recovery callback called!" << endl;
}

The second configuration in config_2 also demonstrates the timeout functionality of the PacketManager. As mentioned in the specification, after 3 times the duration of whatever PacketManager::callback_config_t::timeout is set to—representing 2 missed frames—PacketManager::callback_config_t::timeout_callback will be called. Here, that means you should see the timeout message on your terminal 3 seconds after running the program. If you send a message matching that filter:

cansend vcan0 10000000#11223344

You should see, in order:

  1. The data callback be called (RX callback 2), which will then print the frame data.

  2. The timeout recovery callback get called.

  3. 3 seconds later, the timeout callback message.

Note that although both are provided in this tutorial, you only need to provide one of the timeout or recovery callbacks.

However, if you instead send a frame matching only the first configuration:

cansend vcan0 12340000#11223344

The RX callback 1 function will be called instead, since its filter now matches the frame.

That’s it! You now know how to use the PacketManager to handle receiving data and routing it to a callback.

Transmission Configurations

Note

The program written in this section of the tutorial can be run with:

nix run .#pkgs.examples.hi-can.packet-manager-tx

Much like with the RawCanInterface directly, transmitting data is much simpler than receiving it. To set up an interval transmission, we need a addressing::flagged_address_t for the transmitted frame’s ID and flags, and a valid PacketManager::transmission_config_t for the data and its settings. However, rather than just setting the data directly, transmissions instead call a function called the “data generator” which must return the frame’s data as a std::vector<uint8_t>: PacketManager::transmission_config_t::generator. Although this might seem strange at first (why not just update the data in the PacketManager?), it means that rather than the data source needing to know about the transmission, the transmission knows about the data source, removing code duplication in every potential data generator.

std::vector<uint8_t> data_generator()
{
    static uint8_t counter = 0;
    counter++;
    cout << "Generating data, counter: " << counter << endl;
    const std::vector<uint8_t> data = {counter, 0x11, 0x22, 0x33};
    return data;
}

Putting it all together gives:

#include <signal.h>
#include <unistd.h>

#include <chrono>
#include <hi_can_raw.hpp>
#include <iostream>
#include <string>
#include <thread>
#include <vector>

using namespace hi_can;
using namespace std::chrono_literals;
using std::cout, std::cin, std::endl;

void signal_handler(int signal);
std::vector<uint8_t> data_generator();

static bool running = true;

int main(int argc, const char** argv)
{
    std::string interface_id = "vcan0";
    if (argc > 1)
        interface_id = argv[1];

    // handle CTRL+C cleanly: https://stackoverflow.com/questions/1641182/how-can-i-catch-a-ctrl-c-event
    struct sigaction sigint_handler;
    sigint_handler.sa_handler = signal_handler;
    sigemptyset(&sigint_handler.sa_mask);
    sigint_handler.sa_flags = 0;
    sigaction(SIGINT, &sigint_handler, NULL);

    try
    {
        RawCanInterface can_interface(interface_id);
        cout << "Opened CAN interface: " << interface_id << endl;

        PacketManager packet_manager(can_interface);

        addressing::flagged_address_t address(0x12345678);
        PacketManager::transmission_config_t config = {
            .generator = data_generator,
            .interval = 1s,
        };
        packet_manager.set_transmission_config(address, config);

        while (running)
        {
            packet_manager.handle();
            std::this_thread::sleep_for(10ms);
        }
    }
    catch (const std::exception& e)
    {
        cout << "Error: " << e.what() << endl;
        return 1;
    }

    return 0;
}

void signal_handler(int signal)
{
    (void)signal;  // silence unused variable warning
    cout << endl
         << "SIGINT caught, shutting down..." << endl;
    running = false;
}

std::vector<uint8_t> data_generator()
{
    static uint8_t counter = 0;
    counter++;
    cout << "Generating data, counter: " << counter << endl;
    const std::vector<uint8_t> data = {counter, 0x11, 0x22, 0x33};
    return data;
}

Tip

Run

candump any

to see the frames being transmitted!

The remaining config options are the interval determining the time between frames, and the optional transmission_config_t::should_transmit_immediately flag, which if true will force the frame to be transmitted as soon as the configuration is set, rather than waiting till the interval expires for the first time.

Finally, as with the RawCanInterface, the PacketManager also has split receive and transmit functions: PacketManager::handle_receive() and PacketManager::handle_transmit(). The only difference is that the handle_transmit() function takes a flag which, if set, will force all the registered transmission configurations to be sent immediately, regardless of configuration. Also, since PacketManager::handle() calls both of them internally, if the receive function is blocking, it can no longer transmit any data.