Skip to content

Implementing AES70 Classes#

The AES70 standard defines a set of control classes with RPC methods, events and properties. Properties are accesses via the corresponding methods (e.g. OcaGain::SetGain modifies the Gain property), property changes are notified to controllers using the PropertyChanged event.

Compile-Time Detection in AES70 Control Classes#

In this library, each AES70 control class is provided as a C++ template. The template takes an implementation type as a parameter, and generates the necessary glue code to expose its methods over the AES70 network protocol.

The key feature is that these templates do not require the implementation type to explicitly inherit from a base class or override virtual methods. Instead, they use compile-time detection (a C++ metaprogramming technique) to determine which RPC methods are present in the implementation type.

How It Works#

At compile time, the library inspects the implementation type (Impl) for the presence of methods with particular names and signatures. If a matching method is found, the control class automatically generates the glue code to expose that method via AES70. If no such method is found, the RPC is simply omitted or reported as OcaStatus::NotImplemented at runtime.

Why This Approach?#

  • Flexibility: Implementers are free to provide only the subset of methods that makes sense for their device.

  • Multiple Signature Variants: The detection mechanism can recognize different method forms (synchronous, asynchronous, with different argument type), and generate glue for each. This can also be used to choose which data type to use to represent argument and return values, which can be important for efficiency (e.g. zero-copy decoding) or on embedded platforms where heap allocations might be limited or not possible.

  • Zero Boilerplate: There is no need to inherit from a common base class or manually register methods.

The list of available template classes can be found here.

A first example#

In this example we want to implement OcaGain.

The methods we want to make available are GetRole, GetGain and SetGain. The AES70 standard defines them as follows:

  • OcaStatus OcaRoot::GetRole(OcaString &Role)
  • OcaStatus OcaGain::GetGain(OcaDB &Gain, OcaDB &minGain, OcaDB &maxGain)
  • OcaStatus OcaGain::SetGain(OcaDB Gain)

Parameters which are marked as reference in the standard are output parameters. The return value with type OcaStatus can be used to signal an error. If an error happens, the output parameters are not used.

Template classes will detect if a method has been implemented and also detect its arguments and return values. In our example we make the following decisions:

  • We inherit from aes70::device::object_base which implements a few standard methods. This is generally recommended.
  • We return const char * from GetRole(). The status is then assumed to be OcaStatus::OK.
  • We return OcaStatus::OK from SetGain only if the value is in range, otherwise OcaStatus::ParameterOutOfRange is returned to the caller. If OcaStatus::OK is returned, the template aes70::device::OcaGain will automatically generate a property change event to notify all connected clients of the change of the gain property.
  • We return a 3-element array from GetGain. The library will automatically map that onto the 3 output parameters Gain, minGain and maxGain.
class MyGainImplementation : public aes70::device::object_base
{
public:
  MyGainImplementation(const char *role_)
   : role(role_)
  {}

  const char* GetRole() {
   return role;
  }

  aes70::OcaStatus SetGain(float gain)
  {
    if (!(gain >= 96.0 && gain <= 6.0))
      return OcaStatus::ParameterOutOfRange;

    this->gain = gain;

    return OcaStatus::OK;
  }

  std::array<float, 3> GetGain()
  {
    return { gain, -96.0, 6.0 };
  }
private:
  float gain;
  const char *role;
};

using Gain = aes70::device::OcaGain<MyGainImplementation>;

Possible method signatures#

Some rules of thumb for how to implement methods from the AES70 specification inside the interface classes:

Arguments:

  • Add each argument as specified in the AES70 specification.
  • Give each argument the type as specified in AES70. Here this either means using the 'natural' corresponding type (e.g. std::string for OcaString) or a supported equivalent. See Data Types for more information.
  • If an argument is not required, specify it as aes70::OCP1::ignore which will make sure that this specific argument is not decoded.

Return values:

  • If a method has more than one return value, return them as std::tuple (or std::array if they all have the same type).
  • If the method has one return value, return that value.
  • If a method has no return value, return void if it always succeeds and OcaStatus otherwise.
  • In more complex cases, consult the documentation of aes70::device::Method.

Each method defined in AES70 is supported by libaes70 with various different signatures. They attempt to cover different use-cases. A list of all methods defined by each wrapper class and the corresponding list of different signatures can be found in the API documentation (see e.g. aes70::device::OcaGain and aes70::device::Method).

Where is my object?#

The wrapper classes will store the implementation object. For instance, when defining

using Gain = aes70::device::OcaGain<MyGainImplementation>;

in the above example, an object of class Gain will contain a property of type MyGainImplementation. This property can be accessed using the get() method which returns a reference to the implementation object.

Alternatively, it is also possible to define

using Gain = aes70::device::OcaGain<MyGainImplementation&>;

in which case object of class Gain will store a reference to an object of class MyGainImplementation. This can be used - for example - to store implementations objects in an array and reference them when creating the device tree. This is most useful in embedded devices.

Property Change Events#

When implementing a Setter such as SetGain in the example above, the corresponding property change event will be automatically generated by libaes70. In situations where the property change is not generated by the by a AES70 method call, the property change event can be generated by defining an event structure corresponding to the property.

The type of this event structure is defined by the control class using the naming convention aes70::device::Oca[ControlClass]_[PropertyName]Changed. The name of the property must be On[PropertyName]Changed. Based on the name this event structure will be discovered by the introspection class.

For example, using aes70::device::OcaUint8Sensor to implement a level meter could be done like this:

struct MyLevelImplementation
{
  aes70::device::OcaUint8Sensor_ReadingChanged OnReadingChanged;
  uint8_t reading;
  const char *role;

  MyLevelImplementation(const char *role)
    : role(role)
  {}

  void update_level(uint8_t level)
  {
    reading = level;

    // trigger the property change event
    OnReadingChanged(level);
  }

  const char *GetRole() const
  {
    return role;
  }

  std::array<uint8_t, 3> GetReading() const
  {
    return { reading, 0, 255 };
  }
};

using MyLevel = aes70::device::OcaUint8Sensor<MyLevelImplementation>;

In the above example by calling update_level the meter reading would be updated and a property change event would be generated and sent to all subscribed clients.

See examples/devices/gains_and_levels for an example using property change events to implement level meters.

Using static methods#

An alternative approach for emitting property change events is to use static methods defined in the aes70::device::Oca[ControlClass]_[PropertyName]Changed classes. This can be benefitial because it does not add any additional properties to the control class implementations and reduced memory usage.

The above example could instead be implemented like this:

struct MyLevelImplementation
{
  // Note, this example would work also without this static
  // property. This is because SetGain will generate property
  // change events.
  static const bool has_property_changed_events = true;
  uint8_t reading;
  const char *role;

  MyLevelImplementation(const char *role)
    : role(role)
  {}

  void update_level(uint8_t level)
  {
    reading = level;
  }

  const char *GetRole() const
  {
    return role;
  }

  std::array<uint8_t, 3> GetReading() const
  {
    return { reading, 0, 255 };
  }
};

class MyLevel : public aes70::device::OcaUint8Sensor<MyLevelImplementation>
{
  using Base = aes70::device::OcaUint8Sensor<MyLevelImplementation>;

  MyLevel(const char *role)
    : Base(role)
  {}

  void update_level(uint8_t level)
  {
    Base::get().update_level(level);
    // trigger the property change event
    aes70::device::OcaUint8Sensor_ReadingChanged::valueChanged(this, level);
  }
};

Note

When using this approach to emit property changed events using static methods, make sure to define the static property has_property_changed_events. This properties is required to let libaes70 know that objects of this type will emit property change events.

Asynchronous responses#

In most situations methods called through AES70 immediately complete their operation and a response can be sent back to the client. However, in some cases operations complete asynchronously. This is supported in libaes70 using aes70::async_response_generator. An example of how to use it can be found in examples/devices/async.