-
Notifications
You must be signed in to change notification settings - Fork 4
Description
This issue will be used to track improvements to the device API used by drivers. There's a final API I'd like to achieve, but it seems a little complicated to do at once. So the migration can be done in phases where each phase will provide a useful improvement. Since there are only a handful of drivers, these changes will be simple to apply -- almost mechanical.
Current API
When a driver defines a read-only device, it uses .add_ro_device() which has the following signature:
async fn add_ro_device(&self, name: Base, units: Option<&str>) ->
Result<(ReportReading, Option<Value>)>;ReportReading is a closure which takes a device::Value and sends it to the backend storage. The second value in the returned tuple is the last value reported by the device. If you're using the simple backend, this will be None since the simple backend has no persistence. The Redis backend will return Some value.
To register a settable device, the function is very similar:
async fn add_rw_device(&self, name: Base, units: Option<&str>) ->
Result<(ReportReading, RxDeviceSetting, Option<Value>)>;The main difference is the return value is a 3-tuple. The previous value has been moved to the last position and the second element is now an mpsc::Receiver<(device::Value, oneshot::Sender)>. This Receiver accepts setting requests from clients. This API works, but I'd like it to be a little more strict and remove some of the boilerplate (especially needing to verify the correct type.)
Phase 1
The first phase would be to have the driver specify the desired type of the device, instead of having to convert its value into a device::Value. The signatures to both registration methods would look something like:
async fn add_ro_device<T: Into<device::Value>>(&self, name: Base, units: Option<&str>) ->
Result<(ReportReading<T>, Option<T>)>;
async fn add_rw_device<T: Into<device::Value>>(&self, name: Base, units: Option<&str>) ->
Result<(ReportReading<T>, RxDeviceSetting<T>, Option<T>)>;In this API, the return values are strongly typed to a (non-device::Value) value. We add the requirement that the type T must be a type that can be turned into a device::Value. ReportReading is now a closure that accepts the type T and converts it to a device::Value before sending it on. The handle that accepts settings is now an async closure that waits for the settings and tries to convert it type T. If it can't, the closure will return a TypeError so the driver only sees settings that have been converted to the expected type.
Phase 2
Each time a device reading is reported, the system gives it its own timestamp. There are some drivers which compute values based on a reading. It would be nice if those devices all had the same timestamp. For instance, a weather driver that obtains weather information from a website should use the same timestamp for all the information.
For this phase's API change, you can register a tuple of devices. Each element of the tuple has its own device name and units and type. However, the devices are only associated with the tuple so the ReportReading closure only accepts a tuple of values. The backend must guarantee that all values in the tuple get associated with the same timestamp.
Something like:
let (devices, prev_vals) = core
.add_ro_devices::<(bool, f64)>(
(("enable", None), ("brightness", Some("%")))
)
.await?;and to update the devices:
devices((true, 50.0)).await?;Only read-only devices can be grouped. There is no equivalent API change for settable devices.
Phase 3
When registering a tuple of devices, any device whose type is Option<T> will be optionally saved to the backend. For instance, in my sump pump driver, the state device is boolean and will change from true to false to true as the sump pump turns on and off. But the duty and in-flow devices only get calculated when the pump turns off. So when the pump turns on, the driver would send (true, None, None) and when it turns off, it sends (false, Some(duty), Some(in_flow)).
Comments
Of these three phases, I think phase 2 will be the hardest. Phase 3 should be easy and, in fact, can probably be done while working on phase 2.