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 | |
The
using namespace std::chrono_literals;line allows us to definestd::chrono::steady_clock::durationintervals as numbers with unit suffixes, like10msor5s.
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 | |
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:
The data callback be called (RX callback 2), which will then print the frame data.
The timeout recovery callback get called.
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.