blob: 30c8b88b9b930ee589c04a6cb60d1657b9a826a3 [file] [log] [blame]
.. _seed-0120:
==========================
0120: Sensor Configuration
==========================
.. seed::
:number: 0120
:name: Sensors Config
:status: Open for Comments
:proposal_date: 2023-11-28
:cl: 183150
:authors: Yuval Peress
:facilitator: Taylor Cramer
-------
Summary
-------
This SEED details the configuration aspect of both sensors and the sensor
framework that will reside under the ``pw_sensor`` module. Under this design,
both a ``Sensor`` and a ``Connection`` object will be configurable with the same
API. As such, the ``Configuration`` is a part of both layers for the sensor
stack:
* There exists a ``Configuration`` class which holds the currently requested
configuration.
* A ``Configuration`` is owned by a ``Sensor`` in a 1:1 relationship. Each
sensor only supports 1 configuration.
* The sensor framework (a layer above the ``Sensor`` driver) has the concept of
a ``Connection``. You can open multiple connections to the same sensor and the
framework will handle the multiplexing. Each ``Connection`` owns a
``Configuration`` in a similar 1:1 relationship like the ``Sensor``. The only
difference is that when a ``Connection``'s configuration changes, the
framework arbitrates the multiple ``Connection``\s to produce a single final
``Configuration`` of the ``Sensor``.
.. mermaid::
:alt: Configuration relationship
:align: center
classDiagram
class Configuration
class Sensor
class Connection
class SensorFramework
Sensor "1" *-- "1" Configuration
Connection "1" *-- "1" Configuration
SensorFramework "1" *-- "*" Sensor
SensorFramework "1" *-- "*" Connection
----------
Motivation
----------
Making sensor drivers configurable lends to the reusability of the driver.
Additionally, each ``Connection`` in the sensor framework should be able to
convey the requested configuration of the client. As depicted above, a
``Connection`` will own a single ``Configuration``. Once a change is made, the
framework will process the change and form a union of all the ``Configuration``
objects that are pointed to the same ``Sensor``. This new union will be used as
the single new configuration of the ``Sensor`` and all ``Connection``\s will be
notified of the change.
------------
Design / API
------------
Measurement Types
-----------------
Measurement types include things like *acceleration*, *rotational velocity*,
*magnetic field*, etc. Each type will be described by a ``uint16_t`` hash of the
name and the unit strings each. This makes the measurement type automatically
easy to log in a human readable manner when leveraging tokenized logging.
Additionally, the final measurement type (being the concatination of 2 tokens)
is represented as a ``uint32_t``.
.. code-block:: c++
union MeasurementType {
struct {
uint16_t name_token;
uint16_t unit_token;
}
uint32_t type;
};
#define PW_SENSOR_MEASUREMENT_TYPE(domain, name_str, unit_str) \
{ \
.name_token = \
PW_TOKENIZE_STRING_MASK(domain, 0xffff, name_str), \
.unit_token = \
PW_TOKENIZE_STRING_MASK(domain, 0xffff, unit_str), \
}
Pigweed would include some common measurement types:
.. code-block:: c++
constexpr MeasurementType kAcceleration =
PW_SENSOR_MEASUREMENT_TYPE("PW_SENSOR_MEASUREMENT_TYPE", "acceleration", "m/s2");
constexpr MeasurementType kRotationalVelocity =
PW_SENSOR_MEASUREMENT_TYPE("PW_SENSOR_MEASUREMENT_TYPE", "rotational velocity", "rad/s");
constexpr MeasurementType kMagneticField =
PW_SENSOR_MEASUREMENT_TYPE("PW_SENSOR_MEASUREMENT_TYPE", "magnetic field", "T");
constexpr MeasurementType kStep =
PW_SENSOR_MEASUREMENT_TYPE("PW_SENSOR_MEASUREMENT_TYPE", "step count", "step");
Applications can add their own unique units which will not collide as long as
they have a unique domain, name, or unit representation:
.. code-block:: c++
/// A measurement of how many pancakes something is worth.
constexpr MeasurementType kPancakes =
PW_SENSOR_MEASUREMENT_TYPE("iHOP", "value", "pnks");
Attribute Types
---------------
Attribute types are much simpler that ``MeasurementTypes`` since they derive
their units from the measurement type. Instead, they'll just be
represented via a single token:
.. code-block:: c++
using AttributeType = uint32_t;
#define PW_SENSOR_ATTRIBUTE_TYPE(domain, name_str) \
PW_TOKENIZE_STRING_DOMAIN(domain, name_str)
Similar to the ``MeasurementType``, Pigweed will define a few common attribute
types:
.. code-block:: c++
constexpr AttributeType kOffset =
PW_SENSOR_ATTRIBUTE_TYPE("PW_SENSOR_ATTRIBUTE_TYPE", "offset");
constexpr AttributeType kFullScale =
PW_SENSOR_ATTRIBUTE_TYPE("PW_SENSOR_ATTRIBUTE_TYPE", "full scale");
constexpr AttributeType kSampleRate =
PW_SENSOR_ATTRIBUTE_TYPE("PW_SENSOR_ATTRIBUTE_TYPE", "sample rate");
Attributes
----------
A single ``Attribute`` representation is the combination of 3 fields:
measurement type, attribute type, and value.
.. code-block:: c++
class Attribute : public pw::IntrusiveList<Attribute>::Item {
public:
Attribute(MeasurementType measurement_type, AttributeType attribute_type)
: measurement_type(measurement_type), attribute_type(attribute_type) {}
bool operator==(const Attribute& rhs) const {
return measurement_type == rhs.measurement_type &&
attribute_type == rhs.attribute_type &&
memcmp(data, rhs.data, sizeof(data)) == 0;
}
Attribute& operator=(const Attribute& rhs) {
PW_DASSERT(measurement_type == rhs.measurement_type);
PW_DASSERT(attribute_type == rhs.attribute_type);
memcpy(data, rhs.data, sizeof(data));
return *this;
}
template <typename T>
void SetValue(typename std::enable_if<std::is_integral_v<T> ||
std::is_floating_point_v<T>,
T>::type value) {
memcpy(data, value, sizeof(T));
}
template <typename T>
typename std::enable_if<std::is_integral_v<T> ||
std::is_floating_point_v<T>,
T>::type GetValue() {
return *static_cast<T*>(data);
}
MeasurementType measurement_type;
AttributeType attribute_type;
private:
std::byte data[sizeof(long double)];
};
Configuration
-------------
A configuration is simply a list of attributes. Developers will have 2 options
for accessing and manipulating configurations. The first is to create the
sensor's desired configuration and pass it to ``Sensor::SetConfiguration()``.
The driver will return a ``Future`` using the async API and will attempt to set
the desired configuration. The second option is to first query the sensor's
attribute values, then manipulate them, and finally set the new values using the
same ``Sensor::SetConfiguration()`` function.
.. code-block:: c++
using Configuration = pw::alloc::Vector<Attribute>;
/// @brief A pollable future that returns a configuration
/// This future is used by the Configurable::GetConfiguration function. On
/// success, the content of Result will include the current values of the
/// requester Attribute objects.
class ConfigurationFuture {
public:
pw::async::Poll<pw::Result<Configuration*>> Poll(pw::async::Waker& waker);
};
class Configurable {
public:
/// @brief Get the current values of a configuration
/// The @p configuration will dictate both the measurement and attribute
/// types which are to be queried. The function will return a future and
/// begin performing any required bus transactions. Once complete, the
/// future will resolve and contain a pointer to the original Configuration
/// that was passed into the function, but the values will have been set.
virtual ConfigurationFuture GetConfiguration(
Configuration& configuration) = 0;
/// @brief Set the values in the provided Configuration
/// The driver will attempt to set each attribute in @p configuration. By
/// default, if an attribute isn't supported or the exact value can't be
/// used, the driver will make a best effort by skipping the attribute in
/// the case that it's not supported or rounding it to the closest
/// reasonable value. On success, the function should mutate the attributes
/// to the actual values that were set.
/// For example:
/// Lets assume the driver supports a sample rate of either 12.5Hz or
/// 25Hz, but the caller used 20Hz. Assuming that @p allow_best_effort
/// was set to `true`, the driver is expected to set the sample rate to
/// 25Hz and update the attribute value from 20Hz to 25Hz.
virtual ConfigurationFuture SetConfiguration(
Configuration& configuration, bool allow_best_effort = true) = 0;
};
Memory management
-----------------
In the ``Configurable`` interface we expose 2 functions which allow getting and
setting the configuration via the Pigweed async API. In both cases, the caller
owns the memory of the configuration. It is the caller that is required to
allocate the space of the attributes which they'd like to query or mutate and it
is the caller's responsibility to make sure that those attributes (via the
``Configuration``) do not go out of scope. The future, will not own the
configuration once the call is made, but will hold a pointer to it. This means
that the address must also be stable. If the future goes out of scope, then the
request is assumed canceled, but the memory for the configuration is not
released since the future does not own the memory.
While it's possible to optimize this path a bit further, sensors are generally
not re-configured often. The majority of sensors force some down time and the
loss of some samples while being re-configured. This makes the storage and
mutation of a ``Configuration`` less critical. It would be possible to leverage
a ``FlatMap`` for the ``Configuration`` in order to improve the lookup time.
The biggest drawback to this approach is the lack of dynamic attribute support.
If we want to allow pluggable sensors where attributes are discovered at
runtime, we would not be able to leverage the ``FlatMap``.
Alternatively, if a ``Configuration``'s keys are known at compile time, we
could support the following cases:
* When a ``Sensor`` knows which attributes it supports at compile time, we
should be able to allocate an appropriate ``FlatMap``. When the developer
requests the full configuration, we would copy that ``FlatMap`` out and allow
the consumer to mutate the copy.
* A consumer which only cares about a subset of statically known attributes, can
allocate their own ``FlatMap`` backed ``Configuration``. It would pass a
reference to this object when querying the ``Sensor`` and have the values
copied out into the owned ``Configuration``.
--------------------
Sensor vs. Framework
--------------------
When complete, both the ``Sensor`` and the ``Connection`` [1]_ objects will
inherit from the ``Configurable`` interface. The main differences are that in
the case of the ``Sensor``, the configuration is assumed to be applied directly
to the driver, while in the case of the ``Connection``, the sensor framework
will need to take into account the configurations of other ``Connection``
objects pointing to the same ``Sensor``.
.. [1] A connection is allocated by the sensor framework to the client and
allows clients to request configuration changes.