Implementing caller side

On the caller side, requesting a remote device to invoke a function is pretty much like a regular function invocation. The types of the arguments are checked at compile-time. The type of the return value is also available at compile-time.

What Unpadded does for you is generating the packet and sending it byte by byte through a user-provided callback. What remains to be handled is the implementation of the aforesaid callback.

The following sections will assume these functions to be defined by the caller device:

void write_byte_to_slave(upd::byte_t);
upd::byte_t read_byte_from_slave();

Basics : sending request with keys

Preparing a packet for a request is done by using keys from the corresponding keyring. Let say we defined a keyring object holding the function f of signature void(std::uint8_t, std::uint16_t).

// We create a key to `f`.
auto key = keyring.get<f>();

// Then we generate the packet and write every byte of it
// to the slave device. On the remote device, ``f`` will be
// invoked on the arguments ``16`` and ``64``.
key(16, 64).write_to(write_byte_to_slave);

// This line does the same thing, but with another syntax.
key(16, 64) >> write_byte_to_slave;

If argument types does not match, the compiler will tell you.

std::uint8_t array[] = {0x00, 0x11, 0x22};
auto key = keyring.get<f>();

// What are you doing ?
//      vvvvv
key(16, array).write_all(write_byte_to_master);

Once sent, the packet must be received by a dispatcher.

Example

In the follwing example, we are requesting the callee device to spiral faster and faster.

motion_keyring.hpp
#pragma once

#include <cstdint>

#include <upd/format.hpp>
#include <upd/keyring.hpp>
#include <upd/typelist.hpp>

void set_forward_speed(std::uint32_t);
void set_left_steering_speed(std::uint32_t);
void set_right_steering_speed(std::uint32_t);

constexpr upd::keyring motion_keyring{upd::flist<set_forward_speed, set_left_steering_speed, set_right_steering_speed>,
                                      upd::little_endian,
                                      upd::twos_complement};
master.cpp
#pragma once

#include <cstdint>

#include <upd/format.hpp>
#include <upd/keyring.hpp>
#include <upd/typelist.hpp>

void set_forward_speed(std::uint32_t);
void set_left_steering_speed(std::uint32_t);
void set_right_steering_speed(std::uint32_t);

constexpr upd::keyring motion_keyring{upd::flist<set_forward_speed, set_left_steering_speed, set_right_steering_speed>,
                                      upd::little_endian,
                                      upd::twos_complement};

Handling the callee response

You are free to ignore the result of an invocation request if you don’t need it. Unpadded won’t handle the response from the callee automatically. If you need to, there is two different way to proceed. Either you send a request then wait until the callee device is done (blocking mode) or you hook a callback to be invoked when receiving a response from the callee device (non-blocking mode).

Blocking mode

In blocking mode, keys must be used to extract the result of the invocation request out of the packet sent back by the callee device.

// We assume that a `keyring` object has been defined and
// holds a function `f` of signature `std::uint8_t(std::uint16_t)`.

auto key = keyring.get<f>();

// First we send a request to the slave device
key(64).write_to(write_byte_to_slave);

// Let's wait a little for the slave device to completely
//fulfill the request.

// Then we receive its response
// `x` type is `std::uint8_t`
auto x = key.read_from(read_byte_from_slave);

On bare-metal implementations, the blocking mode is not really useful. If you are using an RTOS however, synchronization primitives can be used in order to wait for the response to be received without blocking the caller device.

Non-blocking mode

If you cannot afford using the blocking mode (for example, because you cannot use an RTOS), Unpadded provides you with a portable way to hook actions to be executed on reception of a reponse from the callee device.

In non-blocking mode, the action class must be used to store a callback for later.

// We assume that a `keyring` object has been defined and
// holds a function `f` of signature `std::uint8_t(std::uint16_t)`

static upd::action action;

void send_request() {
  auto key = keyring.get<f>();

  // We send the request...
  key(64).write_to(write_byte_to_slave);

  // ...and then, we save our callback for later
  action = key.with_hook(my_callback);
}

void process_response() {
  // Our callback is called !
  action(read_byte_from_slave);
}

int main() {
  send_request();

  // Meanwhile, we do other stuff...

  process_response();

  return 0;
}

action is non-templated, so it is suitable for storage. The hooked callback must be invocable on whatever value the remotely called function returns. In case of a multimaster architecture (i.e. if both devices can initiate a request), the buffered_dispatcher::reply() function can come in handy.

API References

key

template<typename Index_T, Index_T Index, typename F, endianess Endianess, signed_mode Signed_Mode>
class key

Packet generator for invoking an action on a callee device.

Keys will generate packets whose structure is the following:

  • index of the action (value: Index; size: sizeof Index);

  • payload (size: sizeof(Args) + ...);

The index is used by the callee to determine which action is addressed. The content of the payload is choosen by the user. In the following example :

int function1(uint8_t, uint16_t);
int function2(uint16_t, uint32_t);
int function3(uint32_t, uint64_t);

constexpr keyring keyring{flist<function1, function2, function3>};

int main() {
  auto key = keyring.get<function2>();
  key(7, 21).write_to(/*an output invocable*/);
}

The packet sent will have the following content:

  • 1 byte of index (value: 1)

  • 2 bytes for the first argument of function2 (type: uint16_t; value: 7)

  • 4 bytes for the second argument of function2 (type: uint32_t; value: 21)

When the packet has been processed by the callee device and the resulting value is sent back to the caller device, keys can be used to extract this value from the callee response. The syntax to achieve that is auto x = key.read_from(/*a byte getter*/). However, key instances being templated, it is hard to store them for later. If blocking the program until the callee device response is not a viable option, it is possible to save a callback to be executed once the callee device has finished using the action or no_storage_action classes.

Template Parameters:
  • Index – Index of the action in the keyring

  • F – Signature of the callback associated with the key

  • Endianess, Signed_Mode – Serialization parameters

Public Types

using index_t = Index_T

Type of the index as found in the packets generated by this key.

using signature_t = R(Args...)

Signature of the invocable associated with this key.

using return_t = detail::remove_cv_ref_t<R>

Return type of read_from() (i.e. the return type of the callback without its reference and cv-qualifier)

using tuple_t = tuple<Endianess, Signed_Mode, detail::remove_cv_ref_t<Args>...>

Type of the tuple which can be invoked on this key (e.g. t.invoke(key))

Public Functions

auto operator()(const Args&... args) const

Generate a packet ready to be sent.

This allows the following syntax : key(x1, x2, x3, ...).write_to(dest) (with dest being a byte putter). dest is invoked on every byte representing the data passed as parameter, in the action they appear in the packet.

Parameters:

args... – Values to insert in the payload

Returns:

a temporary object allowing the syntax mentioned above

template<typename Src>
inline return_t read_from(Src &&src) const

Unserialize a value from a packet sent by a callee device in response to a packet generated by this key.

The following member functions are also defined through CRTP.

R operator<<(Src &&);
R operator<<(Src &&) const;
R read_from(It);
R read_from(It) const;
R operator<<(It);
R operator<<(It) const;
All these functions work the same way as read_from(Src &&).

Template Parameters:
  • Src – Byte getter type

  • It – Input iterator type

Parameters:

src – Byte getter

Returns:

the unserialized value

template<typename F>
inline action with_hook(F &&ftor) const

Plan an action to perform when a packet resulting from the execution of the action is received.

Parameters:

ftor – Callback which will carry out the action

Returns:

an action holding the provided hook

template<typename F, F Ftor>
inline no_storage_action with_hook(unevaluated<F, Ftor>) const

Plan an action to perform when a packet resulting from the execution of the action is received.

Template Parameters:

Ftor – Callback which will carry out the action

Returns:

an action holding the provided hook

template<auto &Ftor>
inline auto with_hook() const

Plan an action to perform when a packet resulting from the execution of the action is received.

Template Parameters:

Ftor – Callback which will carry out the action

Returns:

an action holding the provided hook

Public Static Attributes

static constexpr auto index = Index

Equals the Index template parameter.

static constexpr auto endianess = Endianess

Equals the Endianess template parameter.

static constexpr auto signed_mode = Signed_Mode

Equals the Signed_Mode template parameter.

static constexpr auto payload_length = sizeof(Index_T) + detail::parameters_size<R(Args...)>::value

Equals the length in bytes of an action request produced by this key.

action

class action : public upd::detail::immediate_process<action, void>

Wrapper around a callback which serialize / unserialize parameters and return values.

Given a byte sequence generated by a key instance, an action instance (whose underlying callback has the same signature as the aforesaid key instance) is able to unserialize the parameters from that byte sequence, invoke the underlying callback and serialize the return value as a byte sequence (which can later be unserialized by the same key instance to obtain the return value).

Public Functions

template<endianess Endianess, signed_mode Signed_Mode, typename F>
inline explicit action(F &&ftor, endianess_h<Endianess>, signed_mode_h<Signed_Mode>)

Wrap a copy of a provided callback.

Template Parameters:

Endianess, Signed_Mode – Serialization parameters

Parameters:

ftor – Callback to be wrapped

template<typename Src, typename Dest>
inline void operator()(Src &&src, Dest &&dest) const

Invoke the managed callback.

The parameters are unserialized from the input byte stream src. After the callback invocation, the result is serialized into dest.

The following member functions are also defined through CRTP.

auto operator()(Input &&, Output &&);
auto operator()(Input &&, Output &&) const;
All these functions work the same way as operator()(Src &&, Dest &&).

Template Parameters:
  • Input – Input byte stream type of any kind

  • Output – Output byte stream type of any kind

Parameters:
  • src – Byte getter

  • dest – Byte putter

template<typename Input>
inline void operator()(Input &&input) const

Invoke the managed callback.

Parameters:

input – Input byte stream

inline std::size_t input_size() const

Get the size in bytes of the payload needed to invoke the wrapped callback.

Returns:

The size of the payload in bytes

inline std::size_t output_size() const

Get the size in bytes of the payload representing the return value of the wrapped callback.

Returns:

The size of the payload in bytes

no_storage_action

class no_storage_action : public upd::detail::immediate_process<no_storage_action, void>

Action which does not manage storage for its underlying callback.

no_storage_action instances must be given a free function or a callback with static storage duration, which makes them less permissive than action instances. On the other hand, they do not rely on dynamic allocation, which can be useful for embedded software.

Public Functions

template<typename F, F Ftor, endianess Endianess, signed_mode Signed_Mode>
inline explicit no_storage_action(unevaluated<F, Ftor>, endianess_h<Endianess>, signed_mode_h<Signed_Mode>)

Create an action holding the provided callback.

Template Parameters:
  • Ftor – Free function or callback with static storage duration

  • Endianess, Signed_Mode – Serialization parameters

template<typename Src, typename Dest>
inline void operator()(Src &&src, Dest &&dest) const

Invoke the managed callback.

The parameters are unserialized from the input byte stream src. After the callback invocation, the result is serialized into dest.

The following member functions are also defined through CRTP.

auto operator()(Input &&, Output &&);
auto operator()(Input &&, Output &&) const;
All these functions work the same way as operator()(Src &&, Dest &&).

Template Parameters:
  • Input – Input byte stream type of any kind

  • Output – Output byte stream type of any kind

Parameters:
  • src – Byte getter

  • dest – Byte putter

inline std::size_t input_size() const

Get the size in bytes of the payload needed to invoke the wrapped callback.

Returns:

The size of the payload in bytes

inline std::size_t output_size() const

Get the size in bytes of the payload representing the return value of the wrapped callback.

Returns:

The size of the payload in bytes