kondis 0.3.2

a simple library to communicate with exercise equipment
Documentation
use std::sync::mpsc::Receiver;

use async_trait::async_trait;
use btleplug::{
    api::{CharPropFlags, Characteristic, Peripheral as _},
    platform::Peripheral,
};
use futures::StreamExt;
use uuid::Uuid;

use crate::bluetooth::get_peripheral;
use crate::ftms::FTMSData;
use crate::{Equipment, EquipmentType};

/// A debug bike.
/// Any bluetooth device containing "Console" in its name gets connected to, and every `NOTIFY` characteristic gets subscribed to.
#[derive(Debug, Clone)]
pub struct DebugBike {
    peripheral: Peripheral,
    /// The name of the device
    pub name: String,
    idk: Vec<Characteristic>,
    max_level: i16,
}

#[async_trait]
impl Equipment for DebugBike {
    async fn new(max_level: i16, shutdown_rx: &mut Receiver<()>) -> anyhow::Result<Self> {
        let meta = get_peripheral(EquipmentType::Iconsole0028Bike, shutdown_rx).await?;
        if meta.is_none() {
            return Err(anyhow::anyhow!("No peripheral found"));
        }
        let meta = meta.unwrap();
        Ok(DebugBike {
            peripheral: meta.0,
            name: meta.1,
            idk: Vec::new(),
            max_level,
        })
    }

    async fn connect(&mut self) -> anyhow::Result<bool> {
        let is_connected = self.peripheral.is_connected().await?;
        if !is_connected {
            self.peripheral.connect().await?;
        }
        self.set_characteristics().await?;
        self.subscribe().await?;
        println!("Found and connected to: {}", self.name);
        Ok(self.peripheral.is_connected().await?)
    }

    async fn disconnect(&self) -> anyhow::Result<()> {
        self.cleanup().await?;
        self.peripheral.disconnect().await?;
        Ok(())
    }

    async fn set_target_cadence(&self, rpm: i16) -> anyhow::Result<()> {
        if !(1..=self.max_level).contains(&rpm) {
            return Err(anyhow::anyhow!(
                "RPM must be between 1 and {}",
                self.max_level
            ));
        }
        Ok(())
    }

    async fn set_target_power(&self, watts: i16) -> anyhow::Result<()> {
        if !(1..=self.max_level).contains(&watts) {
            return Err(anyhow::anyhow!(
                "Watts must be between 1 and {}",
                self.max_level
            ));
        }
        Ok(())
    }

    async fn read(&self) -> anyhow::Result<Option<FTMSData>> {
        let (data, _) = self.notifications().await?;
        println!("Received data: {data:?}");

        Ok(Some(FTMSData {
            speed: 0.0,
            cadence: 0.0,
            distance: 0.0,
            resistance: 0.0,
            power: 0,
            calories: 0.0,
            heart_rate: 0.0,
            time: 0,
        }))
    }
}

impl DebugBike {
    async fn cleanup(&self) -> anyhow::Result<()> {
        for characteristic in &self.idk {
            self.peripheral.unsubscribe(characteristic).await?;
        }
        Ok(())
    }

    async fn set_characteristics(&mut self) -> anyhow::Result<()> {
        self.peripheral.discover_services().await?;
        for characteristic in self.peripheral.characteristics() {
            if characteristic.properties.contains(CharPropFlags::NOTIFY) {
                self.idk.push(characteristic.clone());
            }
        }
        Ok(())
    }

    async fn subscribe(&self) -> anyhow::Result<()> {
        for characteristic in &self.idk {
            self.peripheral.subscribe(characteristic).await?;
        }
        Ok(())
    }

    async fn notifications(&self) -> anyhow::Result<(Vec<u8>, Uuid)> {
        let mut notifications = self.peripheral.notifications().await?;
        if let Some(data) = notifications.next().await {
            return Ok((data.value, data.uuid));
        }

        Ok((Vec::new(), Uuid::nil()))
    }
}