0120: Sensor Configuration#
Status: Open for Comments Intent Approved Last Call Accepted Rejected
Proposal Date: 2023-11-28
CL: pwrev/183150
Author: 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 aSensor
in a 1:1 relationship. Each sensor only supports 1 configuration.The sensor framework (a layer above the
Sensor
driver) has the concept of aConnection
. You can open multiple connections to the same sensor and the framework will handle the multiplexing. EachConnection
owns aConfiguration
in a similar 1:1 relationship like theSensor
. The only difference is that when aConnection
’s configuration changes, the framework arbitrates the multipleConnection
s to produce a single finalConfiguration
of theSensor
.
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
.
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:
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:
/// 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:
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:
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.
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.
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 appropriateFlatMap
. When the developer requests the full configuration, we would copy thatFlatMap
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
backedConfiguration
. It would pass a reference to this object when querying theSensor
and have the values copied out into the ownedConfiguration
.
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
.