mq2_pcf8563 0.1.1

No-std MQ-2 gas sensor logic + PCF8563 RTC support
Documentation
// src/lib.rs

// Parts of gas_index (scaling/curve logic) and pad2/pad4 helpers were inspired or adapted from,
// suggestions generated by LLM tools (Grok/ChatGPT). 
// All code was manually reviewed, tested, and refined by me.

#![no_std]

use embedded_hal::i2c::I2c;
use pcf8563::{PCF8563, DateTime, Error as RtcError};
use ufmt::{uWrite, uwrite, uwriteln};

/// Configuration for the gas sensor + RTC logger.
/// 
/// Controls alert level and how much smoothing to apply.
#[derive(Clone, Copy, Debug)]
pub struct GasRtcLoggerConfig {
    /// INDEX value that triggers "HIGH GAS DETECTED!" alert.
    /// 
    /// Typical range: 300–600 (tune based on your tests).
    pub alert_threshold: u16,

    /// How many recent INDEX values to average (smoothing).
    /// 
    /// Higher = smoother but slower to react.
    /// Range: 1–16 (fixed buffer size).
    pub smoothing_samples: usize,
}

/// Driver for MQ-2 gas sensor (analog) + PCF8563/HZ-8563 RTC (I²C).
/// 
/// Main features:
/// - Set clean-air baseline for accurate detection
/// - Calculate smoothed gas INDEX (higher = more gas)
/// - Log timestamp + RAW + INDEX
/// - Alert when INDEX exceeds threshold
pub struct GasRtcLogger<I2C> {
    /// RTC driver for timekeeping.
    rtc: PCF8563<I2C>,

    /// Clean-air ADC baseline (set via `set_baseline`).
    baseline_raw: u16,

    /// User settings (alert level, smoothing).
    config: GasRtcLoggerConfig,

    /// Buffer for smoothing INDEX with moving average.
    last_indices: [u16; 16],

    /// Current position in the buffer.
    index_pos: usize,
}

impl<I2C, E> GasRtcLogger<I2C>
where
    I2C: I2c<Error = E>,
{
    /// Create a new logger.
    /// 
    /// `i2c` is the I²C bus (e.g., from arduino-hal).
    /// `config` sets alert level and smoothing.
    /// RTC is initialized automatically.
    pub fn new(i2c: I2C, config: GasRtcLoggerConfig) -> Self {
        let mut rtc = PCF8563::new(i2c);
        let _ = rtc.rtc_init();

        Self {
            rtc,
            baseline_raw: 0,
            config,
            last_indices: [0; 16],
            index_pos: 0,
        }
    }

    /// Set the clean-air baseline ADC value.
    /// 
    /// Call this once after measuring in fresh air.
    /// Typical values: 50–200.
    pub fn set_baseline(&mut self, baseline: u16) {
        self.baseline_raw = baseline;
    }

    /// Set the RTC date and time.
    /// 
    /// Call once to sync time (e.g., on startup).
    pub fn set_datetime(&mut self, dt: &DateTime) -> Result<(), RtcError<E>> {
        self.rtc.set_datetime(dt)
    }

    /// Get the current date and time from the RTC.
    pub fn get_datetime(&mut self) -> Result<DateTime, RtcError<E>> {
        self.rtc.get_datetime()
    }

    /// Calculate smoothed gas INDEX from current raw ADC value.
    /// 
    /// INDEX = (baseline - raw) × 50, then smoothed with moving average.
    /// Higher INDEX = more gas detected.
    /// Returns 0 if baseline not set.
    pub fn gas_index(&mut self, raw: u16) -> u16 {
        if self.baseline_raw == 0 {
            return 0;
        }

        let delta = self.baseline_raw.saturating_sub(raw);
        let mut index = delta as u32 * 50;
        index = index.min(65_535);

        // Update circular buffer
        self.last_indices[self.index_pos] = index as u16;
        self.index_pos = (self.index_pos + 1) % self.config.smoothing_samples;

        // Average the buffer
        let mut sum: u32 = 0;
        for i in 0..self.config.smoothing_samples {
            sum += self.last_indices[i] as u32;
        }

        (sum / self.config.smoothing_samples as u32) as u16
    }

    /// Check if INDEX is high enough for alert.
    pub fn is_high_gas(&self, index: u16) -> bool {
        index > self.config.alert_threshold
    }

    /// Helper: zero-pad two-digit number for ufmt.
    fn pad2<W: uWrite>(w: &mut W, n: u8) {
        if n < 10 {
            let _ = uwrite!(w, "0{}", n);
        } else {
            let _ = uwrite!(w, "{}", n);
        }
    }

    /// Helper: zero-pad four-digit year for ufmt.
    fn pad4<W: uWrite>(w: &mut W, n: u16) {
        if n < 10 {
            let _ = uwrite!(w, "000{}", n);
        } else if n < 100 {
            let _ = uwrite!(w, "00{}", n);
        } else if n < 1000 {
            let _ = uwrite!(w, "0{}", n);
        } else {
            let _ = uwrite!(w, "{}", n);
        }
    }

    /// Log one reading with timestamp.
    /// 
    /// Format: `YYYY-MM-DD HH:MM:SS | RAW=xxx INDEX=xxx`
    /// If INDEX > threshold, adds "ALERT: HIGH GAS DETECTED!"
    pub fn log<W: uWrite>(&mut self, serial: &mut W, raw: u16) {
        if let Ok(dt) = self.get_datetime() {
            Self::pad4(serial, dt.year as u16 + 2000);
            let _ = uwrite!(serial, "-");
            Self::pad2(serial, dt.month);
            let _ = uwrite!(serial, "-");
            Self::pad2(serial, dt.day);
            let _ = uwrite!(serial, " ");
            Self::pad2(serial, dt.hours);
            let _ = uwrite!(serial, ":");
            Self::pad2(serial, dt.minutes);
            let _ = uwrite!(serial, ":");
            Self::pad2(serial, dt.seconds);
            let _ = uwrite!(serial, " | ");
        } else {
            let _ = uwrite!(serial, "RTC_ERR | ");
        }

        let index = self.gas_index(raw);
        let _ = uwriteln!(serial, "RAW={} INDEX={}", raw, index);

        if self.is_high_gas(index) {
            let _ = uwriteln!(serial, "ALERT: HIGH GAS DETECTED!");
        }
    }
}