opensleep: Rust firmware for the Eight Sleep Pod 3 (replacing ALL of Eight Sleep's proprietary programs)

204 points liamsnow_03 24 comments

Hi r/rust!

Github Link

I have spent the last many months working on one of my favorite Rust project to date - a complete firmware for the Eight Sleep Pod 3 that replaces Frank/frankenfirmware, DAC/PizzaRat, and Capybara (IE replaces all of Eight Sleep's programs running on the SOM).

With opensleep you can use your Pod 3 with complete privacy and make cool Home Assistant automations for when you get in and out of bed.

Personally I have it set up to read my daily calendar when I get out of bed in the morning and remind to go to bed when its late.

Background:
For some quick background, the Eight Sleep Pod 3 is temperature controller mattress cover with sleep tracking. Normally all raw sensor data is send to the cloud to be processed and converted into sleep tracking data. You view this data and control the mattress via the mobile app.

Projects like ninesleep, freesleep, and this one all run programs on the Varisite SOM running a minimal Yocto Musl Linux build. Having your own firmware gives you some nice benefits like privacy (by not sending data to the cloud), more control over temp alarms etc, and allow you to add more features.

How Other Projects Work:
My old version of opensleep, ninesleep, and freesleep all work by pretending to be the DAC (the interface to the Eight Sleep servers) by communicating with Frank over a Unix socket.

Frank then manages communication to two "subsystems" called "Sensor" (the STM32 on the sensor unit communicated by over USART via the USB cable) and "Frozen" (an onboard STM32 for managing water pumps, TECs, solenoids, etc.). Capybara's purpose isn't entirely clear to me but its seems to handle initial Bluetooth setup along with I2C control of the LED controller and an IO expander for resetting and enabling Frozen.

While communicating with Frank works totally fine, it limits your ability to get real-time sensor data. I really really wanted to create Home Assistant automations for when I get in and out of bed so I decided to go down this really annoying but also interesting journey.

Features:

  1. Custom temperature profile. Define as many points as you want. If you just want a constant temperature you can do that, or if you want to interpolate between 100 different temperatures in the night you can do that too.
  2. Vibration alarms
  3. Presence detection using capacitance sensors
  4. Couples and one user modes
  5. LED control & cool effects
  6. Daily priming
  7. MQTT interface for remotely updating configuration and reading state (see README for spec)
  8. Configured via a Ron file

Future Plans:
In the future I plan to expand this project with sleep tracking and better temperature control (changing Frozen's setpoints based off bed temperature sensor readings).

Technical:
I wanted to post this project here on r/rust first, because there was a ton of decisions I made and I'm not sure they are the best approach. I would love to get some feedback on how I could improve this:

  1. For serial and packets, I decided to make a "PacketCodec" for use in a framed serial port. Basically you end up with a `Framed<SerialStream, PacketCodec<P>>` where you can just call `reader.next()` and get packets directly. This method proved to be much more reliable than my other attempts (reading until I get a start byte, then reading the whole packet and consuming all bytes) because it wont consume bytes unless it knows it found a valid packet. This makes it more reliable because sometimes it would get stuck in a loop where I find a start byte in the middle of a packet, read it like a packet consuming all the bytes, and then keep hoping around between the middle of packets. You can see the codec in `src/common/codec.rs`.
  2. Originally I designed the Sensor and Frozen managers (src/{sensor,frozen}/manager.rs) to spawn a bunch of tokio tasks each handling their own thing. For example, for frozen I would have a task to manage the left temp, one to get hwinfo, etc. This then required than I would need the state manager to be inside an Arc<Mutex<_>> so all tasks could share state. While this works fine, I ended up changing to a different approach that gets away from this. My approach does everything in one thread by leveraging `tokio::select` and checking if we have a command to send out every 20ms (commands have priorities over others). Frozen keeps this really simple and Sensor has a whole scheduler system.
  3. Originally Frozen and Sensor would have mpsc channels to send all their updates to a central MQTT thread that would then collect all of this and publish it. I switched to giving Frozen and Sensor the MQTT `AsyncClient` and have them publish directly. I switched to this approach after reading that it can be used this way from the docs.
  4. I intentionally designed it so there are only 3 tokio tasks now. The main thread has a tokio select on them, so if one fails they all fail. Then all restarting logic is handled by systemd.
  5. Currently the presence sensor uses a simple debouncer, but I was thinking about moving to a different system where its easier to stay in the current state and needs clear signals to transition.
  6. Anything other comments are very appreciated!

I spent a good amount of time trying to make my code decently fast, but in reality it doesn't really matter. The VAR-SOM-MX8M-MINI_V1.3 is way overkill and runs opensleep at 0.3-0.6% CPU and 0.1% RAM usage.

Notice:
This project is purely intended educational and research purposes. It is for personal, non-commercial use only. It is not affiliated with, endorsed by, or sponsored by Eight Sleep. The Eight Sleep name and Pod are trademarks of Eight Sleep, Inc. (plz don't sue me)