view rust/src/fridge.rs @ 634:a5721c02d3ee rust

build succeeds
author Matt Johnston <matt@ucc.asn.au>
date Sun, 22 Sep 2019 20:35:40 +0800
parents 490e9e15b98c
children 4424a8b30f9c
line wrap: on
line source

// TODO:
// - riker
//   - use monotonic clock
//   - timer.rs should use rx.recv_timeout(next_time) instead of rx.try_recv()
//       and then could remove cfg.frequency_millis
use std;

use std::time::{Duration,Instant};
use riker::actors::*;

#[cfg(target_os = "linux")]
use self::sysfs_gpio::{Direction, Pin};

use super::config::Config;
use super::params::Params;
use super::types::*;

#[derive(Debug,Clone)]
pub struct Tick;

#[actor(Params, Tick, Readings)]
pub struct Fridge {
    params: Params,
    config: Config,

    on: bool,
    temp_wort: Option<f32>,
    temp_fridge: Option<f32>,
    last_off_time: Instant,
    wort_valid_time: Instant,
    integrator: StepIntegrator,
    control: FridgeControl,
}

impl Actor for Fridge {
    type Msg = FridgeMsg;
    fn recv(&mut self,
                ctx: &Context<Self::Msg>,
                msg: Self::Msg,
                sender: Sender) {
        self.receive(ctx, msg, sender);
        // TODO: should we do self.tick(ctx) here instead?
    }

    fn pre_start(&mut self, ctx: &Context<Self::Msg>) {
        let chan: ChannelRef<Readings> = channel("readings", &ctx.system).unwrap();
        let sub = Box::new(ctx.myself());
        chan.tell(Subscribe {actor: sub, topic: "readings".into()}, None);

        let chan: ChannelRef<Params> = channel("params", &ctx.system).unwrap();
        let sub = Box::new(ctx.myself());
        chan.tell(Subscribe {actor: sub, topic: "params".into()}, None);

        self.tick(ctx);
    }
}

impl Receive<Readings> for Fridge {
    type Msg = FridgeMsg;
    fn receive(&mut self,
                ctx: &Context<Self::Msg>,
                r: Readings,
                _sender: Sender) {
        self.temp_wort = r.get_temp(&self.config.WORT_NAME);
        self.temp_fridge = r.get_temp(&self.config.FRIDGE_NAME);

        if self.temp_wort.is_some() {
            self.wort_valid_time = Instant::now();
        }

        self.tick(ctx);
    }
}

impl Receive<Params> for Fridge {
    type Msg = FridgeMsg;
    fn receive(&mut self,
                ctx: &Context<Self::Msg>,
                p: Params,
                _sender: Sender) {
        self.params = p;
        println!("fridge set_params {:?}", self.params);

        self.tick(ctx);
    }
}

impl Receive<Tick> for Fridge {
    type Msg = FridgeMsg;
    fn receive(&mut self,
                ctx: &Context<Self::Msg>,
                _tick: Tick,
                _sender: Sender) {
        self.tick(ctx);
    }
}

enum FridgeControl {
#[cfg(target_os = "linux")]
    Gpio(Pin),
    Fake,
}

impl Drop for Fridge {
    fn drop(&mut self) {
        // safety fridge off 
        self.turn(false);
    }
}

impl Fridge {
    pub fn new_actor((config, nowait) : (Config, bool)) -> Fridge {
        Self::new(config, nowait)

    }
    pub fn new(config: Config, nowait: bool) -> Fridge {
        let mut f = Fridge { 
            config: config.clone(),
            params: Params::defaults(),
            on: false,
            temp_wort: None,
            temp_fridge: None,
            last_off_time: Instant::now(),
            wort_valid_time: Instant::now() - Duration::new(config.FRIDGE_WORT_INVALID_TIME, 100),
            integrator: StepIntegrator::new(Duration::new(1, 0)),
            control: Self::make_control(&config),
        };

        if nowait {
            f.last_off_time -= Duration::new(config.FRIDGE_DELAY, 1);
        }

        f
    }

#[cfg(target_os = "linux")]
    fn make_control(config: &Config) -> FridgeControl {
        let mut pin = Pin(config.FRIDGE_GPIO_PIN);
        // XXX better error handling?
        pin.export().expect("Exporting fridge gpio failed");
        pin.set_direction(Direction::Low).expect("Fridge gpio direction failed");
        FridgeControl::Gpio(pin)
    }

#[cfg(not(target_os = "linux"))]
    fn make_control(_config: &Config) -> FridgeControl {
            FridgeControl::Fake
    }

    fn next_wakeup(&self) -> Duration {
        let millis = 400;
        let dur = Duration::from_millis(millis);
        dur
    }


    fn turn_off(&mut self) {
        info!("Turning fridge off");
        self.turn(false);
    }

    fn turn_on(&mut self) {
        info!("Turning fridge on");
        self.turn(true);
    }

    fn turn(&mut self, on: bool) {
        match self.control {
#[cfg(target_os = "linux")]
            Gpio(pin) => pin.set_value(on as u8),
            FridgeControl::Fake => debug!("fridge turns {}", if on {"on"} else {"off"}),
        }
        self.on = on;
    }

    // Turns the fridge off and on
    fn compare_temperatures(&mut self) {
        let fridge_min = self.params.fridge_setpoint - self.params.fridge_range_lower;
        let fridge_max = self.params.fridge_setpoint - self.params.fridge_range_upper;
        let wort_max = self.params.fridge_setpoint + self.params.fridge_difference;
        let off_time = Instant::now() - self.last_off_time;

        // Or elsewhere?
        self.integrator.set_limit(Duration::new(self.params.overshoot_delay, 0));

        // Safety to avoid bad things happening to the fridge motor (?)
        // When it turns off don't start up again for at least FRIDGE_DELAY
        if !self.on && off_time < Duration::new(self.config.FRIDGE_DELAY, 0) {
            info!("fridge skipping, too early");
            return;
        }

        if self.params.disabled {
            if self.on {
                info!("Disabled, turning fridge off");
                self.turn_off();
            }
            return;
        }

        // handle broken wort sensor
        if self.temp_wort.is_none() {
            let invalid_time = Instant::now() - self.wort_valid_time;
            warn!("Invalid wort sensor for {:?} secs", invalid_time);
            if invalid_time < Duration::new(self.config.FRIDGE_WORT_INVALID_TIME, 0) {
                warn!("Has only been invalid for {:?}, waiting", invalid_time);
                return;
            }
        }

        if self.temp_fridge.is_none() {
            warn!("Invalid fridge sensor");
        }

        if self.on {
            debug!("fridge is on");
            let on_time = self.integrator.integrate().as_secs() as f32;
            let on_ratio = on_time / self.params.overshoot_delay as f32;

            let overshoot = self.params.overshoot_factor as f32 * on_ratio;
            debug!("on_percent {}, overshoot {}", on_ratio * 100.0, overshoot);

            let mut turn_off = false;
            if self.temp_wort.is_some() && !self.params.nowort {
                let t = self.temp_wort.unwrap();
                // use the wort temperature
                if t - overshoot < self.params.fridge_setpoint {
                    info!("wort has cooled enough, {temp}º (overshoot {overshoot}º = {factor} × {percent}%)",
                         temp = t, overshoot = overshoot,
                         factor = self.params.overshoot_factor,
                         percent = on_ratio*100.0);
                    turn_off = true;
                }
            } else if let Some(t) = self.temp_fridge {
                // use the fridge temperature
                if t < fridge_min {
                    warn!("fridge off fallback, fridge {}, min {}", t, fridge_min);
                    if self.temp_wort.is_none() {
                        warn!("wort has been invalid for {:?}", Instant::now() - self.wort_valid_time);
                    }
                    turn_off = true;
                }
            }
            if turn_off {
                self.turn_off();
            }
        } else {
            debug!("fridge is off. fridge {:?} max {:?}. wort {:?} max {:?}",
                self.temp_fridge, fridge_max, self.temp_wort, wort_max);
            let mut turn_on = false;
            if self.temp_wort.is_some() && !self.params.nowort {
                // use the wort temperature
                let t = self.temp_wort.unwrap();
                if t >= wort_max {
                    info!("Wort is too hot {}°, max {}°", t, wort_max);
                    turn_on = true;
                }
            } 

            if let Some(t) = self.temp_fridge {
                if t >= fridge_max {
                    warn!("fridge too hot fallback, fridge {}°, max {}°", t, fridge_max);
                    turn_on = true;
                }
            }

            if turn_on {
                self.turn_on()
            }
        }
    }

    /// Must be called after every state change. Turns the fridge on/off as required and
    /// schedules any future wakeups based on the present (new) state
    /// Examples of wakeups events are
    /// 
    ///  * overshoot calculation
    ///  * minimum fridge-off time
    ///  * invalid wort timeout
    /// All specified in next_wakeup()
    fn tick(&mut self,
        ctx: &Context<<Self as Actor>::Msg>) {
        debug!("tick");

        self.compare_temperatures();

        // Sets the next self-wakeup timeout
        let dur = self.next_wakeup();
        ctx.schedule_once(dur, ctx.myself(), None, Tick);
    }
}