• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1.. _seed-0120:
2
3==========================
40120: Sensor Configuration
5==========================
6.. seed::
7   :number: 0120
8   :name: Sensors Config
9   :status: Open for Comments
10   :proposal_date: 2023-11-28
11   :cl: 183150
12   :authors: Yuval Peress
13   :facilitator: Taylor Cramer
14
15-------
16Summary
17-------
18This SEED details the configuration aspect of both sensors and the sensor
19framework that will reside under the ``pw_sensor`` module. Under this design,
20both a ``Sensor`` and a ``Connection`` object will be configurable with the same
21API. As such, the ``Configuration`` is a part of both layers for the sensor
22stack:
23
24* There exists a ``Configuration`` class which holds the currently requested
25  configuration.
26* A ``Configuration`` is owned by a ``Sensor`` in a 1:1 relationship. Each
27  sensor only supports 1 configuration.
28* The sensor framework (a layer above the ``Sensor`` driver) has the concept of
29  a ``Connection``. You can open multiple connections to the same sensor and the
30  framework will handle the multiplexing. Each ``Connection`` owns a
31  ``Configuration`` in a similar 1:1 relationship like the ``Sensor``. The only
32  difference is that when a ``Connection``'s configuration changes, the
33  framework arbitrates the multiple ``Connection``\s to produce a single final
34  ``Configuration`` of the ``Sensor``.
35
36.. mermaid::
37   :alt: Configuration relationship
38   :align: center
39
40   classDiagram
41       class Configuration
42       class Sensor
43       class Connection
44       class SensorFramework
45       Sensor "1" *-- "1" Configuration
46       Connection "1" *-- "1" Configuration
47       SensorFramework "1" *-- "*" Sensor
48       SensorFramework "1" *-- "*" Connection
49
50----------
51Motivation
52----------
53Making sensor drivers configurable lends to the reusability of the driver.
54Additionally, each ``Connection`` in the sensor framework should be able to
55convey the requested configuration of the client. As depicted above, a
56``Connection`` will own a single ``Configuration``. Once a change is made, the
57framework will process the change and form a union of all the ``Configuration``
58objects that are pointed to the same ``Sensor``. This new union will be used as
59the single new configuration of the ``Sensor`` and all ``Connection``\s will be
60notified of the change.
61
62------------
63Design / API
64------------
65
66Measurement Types
67-----------------
68Measurement types include things like *acceleration*, *rotational velocity*,
69*magnetic field*, etc. Each type will be described by a ``uint16_t`` hash of the
70name and the unit strings each. This makes the measurement type automatically
71easy to log in a human readable manner when leveraging tokenized logging.
72Additionally, the final measurement type (being the concatination of 2 tokens)
73is represented as a ``uint32_t``.
74
75.. code-block:: c++
76
77   union MeasurementType {
78     struct {
79       uint16_t name_token;
80       uint16_t unit_token;
81     }
82     uint32_t type;
83   };
84
85   #define PW_SENSOR_MEASUREMENT_TYPE(domain, name_str, unit_str)              \
86     {                                                                         \
87       .name_token =                                                           \
88           PW_TOKENIZE_STRING_MASK(domain, 0xffff, name_str),                  \
89       .unit_token =                                                           \
90           PW_TOKENIZE_STRING_MASK(domain, 0xffff, unit_str),                  \
91     }
92
93Pigweed would include some common measurement types:
94
95.. code-block:: c++
96
97   constexpr MeasurementType kAcceleration =
98       PW_SENSOR_MEASUREMENT_TYPE("PW_SENSOR_MEASUREMENT_TYPE", "acceleration", "m/s2");
99   constexpr MeasurementType kRotationalVelocity =
100       PW_SENSOR_MEASUREMENT_TYPE("PW_SENSOR_MEASUREMENT_TYPE", "rotational velocity", "rad/s");
101   constexpr MeasurementType kMagneticField =
102       PW_SENSOR_MEASUREMENT_TYPE("PW_SENSOR_MEASUREMENT_TYPE", "magnetic field", "T");
103   constexpr MeasurementType kStep =
104       PW_SENSOR_MEASUREMENT_TYPE("PW_SENSOR_MEASUREMENT_TYPE", "step count", "step");
105
106Applications can add their own unique units which will not collide as long as
107they have a unique domain, name, or unit representation:
108
109.. code-block:: c++
110
111   /// A measurement of how many pancakes something is worth.
112   constexpr MeasurementType kPancakes =
113       PW_SENSOR_MEASUREMENT_TYPE("iHOP", "value", "pnks");
114
115Attribute Types
116---------------
117Attribute types are much simpler that ``MeasurementTypes`` since they derive
118their units from the measurement type. Instead, they'll just be
119represented via a single token:
120
121.. code-block:: c++
122
123   using AttributeType = uint32_t;
124
125   #define PW_SENSOR_ATTRIBUTE_TYPE(domain, name_str)                          \
126       PW_TOKENIZE_STRING_DOMAIN(domain, name_str)
127
128Similar to the ``MeasurementType``, Pigweed will define a few common attribute
129types:
130
131.. code-block:: c++
132
133   constexpr AttributeType kOffset =
134       PW_SENSOR_ATTRIBUTE_TYPE("PW_SENSOR_ATTRIBUTE_TYPE", "offset");
135   constexpr AttributeType kFullScale =
136       PW_SENSOR_ATTRIBUTE_TYPE("PW_SENSOR_ATTRIBUTE_TYPE", "full scale");
137   constexpr AttributeType kSampleRate =
138       PW_SENSOR_ATTRIBUTE_TYPE("PW_SENSOR_ATTRIBUTE_TYPE", "sample rate");
139
140Attributes
141----------
142A single ``Attribute`` representation is the combination of 3 fields:
143measurement type, attribute type, and value.
144
145.. code-block:: c++
146
147   class Attribute : public pw::IntrusiveList<Attribute>::Item {
148    public:
149     Attribute(MeasurementType measurement_type, AttributeType attribute_type)
150         : measurement_type(measurement_type), attribute_type(attribute_type) {}
151
152     bool operator==(const Attribute& rhs) const {
153       return measurement_type == rhs.measurement_type &&
154              attribute_type == rhs.attribute_type &&
155              memcmp(data, rhs.data, sizeof(data)) == 0;
156     }
157
158     Attribute& operator=(const Attribute& rhs) {
159       PW_DASSERT(measurement_type == rhs.measurement_type);
160       PW_DASSERT(attribute_type == rhs.attribute_type);
161       memcpy(data, rhs.data, sizeof(data));
162       return *this;
163     }
164
165     template <typename T>
166     void SetValue(typename std::enable_if<std::is_integral_v<T> ||
167                                               std::is_floating_point_v<T>,
168                                           T>::type value) {
169       memcpy(data, value, sizeof(T));
170     }
171
172     template <typename T>
173     typename std::enable_if<std::is_integral_v<T> ||
174                                               std::is_floating_point_v<T>,
175                                           T>::type GetValue() {
176       return *static_cast<T*>(data);
177     }
178
179     MeasurementType measurement_type;
180     AttributeType attribute_type;
181
182    private:
183     std::byte data[sizeof(long double)];
184   };
185
186Configuration
187-------------
188A configuration is simply a list of attributes. Developers will have 2 options
189for accessing and manipulating configurations. The first is to create the
190sensor's desired configuration and pass it to ``Sensor::SetConfiguration()``.
191The driver will return a ``Future`` using the async API and will attempt to set
192the desired configuration. The second option is to first query the sensor's
193attribute values, then manipulate them, and finally set the new values using the
194same ``Sensor::SetConfiguration()`` function.
195
196.. code-block:: c++
197
198   using Configuration = pw::alloc::Vector<Attribute>;
199
200   /// @brief A pollable future that returns a configuration
201   /// This future is used by the Configurable::GetConfiguration function. On
202   /// success, the content of Result will include the current values of the
203   /// requester Attribute objects.
204   class ConfigurationFuture {
205    public:
206     pw::async::Poll<pw::Result<Configuration*>> Poll(pw::async::Waker& waker);
207   };
208
209   class Configurable {
210    public:
211     /// @brief Get the current values of a configuration
212     /// The @p configuration will dictate both the measurement and attribute
213     /// types which are to be queried. The function will return a future and
214     /// begin performing any required bus transactions. Once complete, the
215     /// future will resolve and contain a pointer to the original Configuration
216     /// that was passed into the function, but the values will have been set.
217     virtual ConfigurationFuture GetConfiguration(
218         Configuration& configuration) = 0;
219
220     /// @brief Set the values in the provided Configuration
221     /// The driver will attempt to set each attribute in @p configuration. By
222     /// default, if an attribute isn't supported or the exact value can't be
223     /// used, the driver will make a best effort by skipping the attribute in
224     /// the case that it's not supported or rounding it to the closest
225     /// reasonable value. On success, the function should mutate the attributes
226     /// to the actual values that were set.
227     /// For example:
228     ///   Lets assume the driver supports a sample rate of either 12.5Hz or
229     ///   25Hz, but the caller used 20Hz. Assuming that @p allow_best_effort
230     ///   was set to `true`, the driver is expected to set the sample rate to
231     ///   25Hz and update the attribute value from 20Hz to 25Hz.
232     virtual ConfigurationFuture SetConfiguration(
233         Configuration& configuration, bool allow_best_effort = true) = 0;
234   };
235
236Memory management
237-----------------
238In the ``Configurable`` interface we expose 2 functions which allow getting and
239setting the configuration via the Pigweed async API. In both cases, the caller
240owns the memory of the configuration. It is the caller that is required to
241allocate the space of the attributes which they'd like to query or mutate and it
242is the caller's responsibility to make sure that those attributes (via the
243``Configuration``) do not go out of scope. The future, will not own the
244configuration once the call is made, but will hold a pointer to it. This means
245that the address must also be stable. If the future goes out of scope, then the
246request is assumed canceled, but the memory for the configuration is not
247released since the future does not own the memory.
248
249While it's possible to optimize this path a bit further, sensors are generally
250not re-configured often. The majority of sensors force some down time and the
251loss of some samples while being re-configured. This makes the storage and
252mutation of a ``Configuration`` less critical. It would be possible to leverage
253a ``FlatMap`` for the ``Configuration`` in order to improve the lookup time.
254The biggest drawback to this approach is the lack of dynamic attribute support.
255If we want to allow pluggable sensors where attributes are discovered at
256runtime, we would not be able to leverage the ``FlatMap``.
257
258Alternatively, if a ``Configuration``'s keys are known at compile time, we
259could support the following cases:
260
261* When a ``Sensor`` knows which attributes it supports at compile time, we
262  should be able to allocate an appropriate ``FlatMap``. When the developer
263  requests the full configuration, we would copy that ``FlatMap`` out and allow
264  the consumer to mutate the copy.
265* A consumer which only cares about a subset of statically known attributes, can
266  allocate their own ``FlatMap`` backed ``Configuration``. It would pass a
267  reference to this object when querying the ``Sensor`` and have the values
268  copied out into the owned ``Configuration``.
269
270--------------------
271Sensor vs. Framework
272--------------------
273When complete, both the ``Sensor`` and the ``Connection`` [1]_ objects will
274inherit from the ``Configurable`` interface. The main differences are that in
275the case of the ``Sensor``, the configuration is assumed to be applied directly
276to the driver, while in the case of the ``Connection``, the sensor framework
277will need to take into account the configurations of other ``Connection``
278objects pointing to the same ``Sensor``.
279
280.. [1] A connection is allocated by the sensor framework to the client and
281   allows clients to request configuration changes.
282