TimOrme / aqimon

Simple Air Quality Monitor For Raspberry Pi and the SDS011 Nova PM Sensor
MIT License
4 stars 2 forks source link

Full implementation of SDS011 spec #30

Closed TimOrme closed 1 year ago

TimOrme commented 1 year ago

This is large one, but fully implements the SDS011 spec from here: https://cdn.sparkfun.com/assets/parts/1/2/2/7/5/Laser_Dust_Sensor_Control_Protocol_V1.3.pdf

Since we can now sleep/wake the device natively, it removes the dependency on uhubctl.

I think ideally, we'll move this into its own package at some point. I tried to separate it out into its own module with that intention.

I'm planning on adding tests before I merge this PR, but would be great to get your feedback on it even in this somewhat early state @inactivist.

TimOrme commented 1 year ago

Ill try to elucidate some of the goals and challenges here.

I wanted to try and do a full implementation of the device spec; prior implementation was a bit hacked together, and one of the nice features of the device is the ability to turn it off and on natively, which can extend the overall life of the device, and generally is nice to avoid the constant, low-volume buzz it would emit otherwise. Before having this functionality available, I was relying on uhubctl to turn off the entire USB port, which is a bit of a sledgehammer for the use case, and also pulled in an external system dependency that was harder to manage.

The PR here implements the full spec, since after finding it I also discovered some other potential nice-to-haves, and it was relatively short to implement. Immediately though, this allows us to remove the ugly dependency of uhubctl and instead do everything directly on the device.

In trying to implement the spec, there were a few challenges however. One particular problem is that it can operate in two reporting modes, either ACTIVE or QUERYING.

In ACTIVE mode, (which is the factory default) the device basically just writes out the diode readings to its buffer semi-continuously, so that no matter what you ask back from the device, it would theoretically give you back air quality readings. As an example if I did something like:

sensor.get_sleep_state()

In ACTIVE mode, the device would instead report back air quality readings, instead of the sleep state like I asked. This in itself is actually fine. I think the intent of ACTIVE mode is that most users don't care to do these more complex things, they just want the darn air quality, without even having to ask for it.

Where ACTIVE mode gets tricky is that it only updates the output semi frequently, which means that if you time it right, calling sensor.get_sleep_state() in ACTIVE mode, might actually return the right thing instead of the air quality reading. I'm guessing a bit, but I think this is because to get sleep state is actually a request we send to the device. The order of operations is something like:

  1. client tells device to write sleep state to output buffer
  2. device writes sleep state to output buffer 2.5 device may write air quality readings to the output buffer
  3. client reads the output buffer

So its a bit of a race condition, leading to very inconsistent results when trying to issue commands in active mode.

QUERYING mode is much more predictable. The device doesn't do anything unless you ask, even write air quality back. In querying mode you have to do something like:

  1. client tells device to write airt quality to output buffer
  2. device writes air quality readings to output buffer
  3. client reads the air quality readings to the output buffer

Which is much simpler, as you can imagine.

The hard part, which is entirely self-imposed, is that I wanted to try and implement the spec in an unopinionated way, such that others could leverage it, even though for our use case, QUERYING mode is the way to go. But, designing a sensible, well-typed, API around these two modes is kind of hard, and so for now I've opted to "support" active mode, but often the API will throw exceptions when using it that are up to the user to handle.

My latest thinking is to leave the implementation I have as it is, so that others have this janky, but unopinionated path open to them, but then to also write two classes on top of this that are opinionated about the mode, and handle the complexity of the jankiness of the underlying API.

Long winded essay, but figured I'd share my thoughts.