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