view rust/src/fridge.rs @ 639:89818a14648b rust tip

- switch to using anyhow for errors, surf for http runs but surf has problems
author Matt Johnston <matt@ucc.asn.au>
date Thu, 28 Nov 2019 23:57:00 +0800
parents a9f353f488d0
children
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 crate::params::Params;
use super::config::Config;
use super::params;
use super::sensor;
use super::types::*;

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

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

    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 params_chan : ChannelRef<Params> = channel("params", ctx).unwrap();
        let sensor_chan : ChannelRef<Readings> = channel("readings", ctx).unwrap();
        let sub = Box::new(ctx.myself());
        params_chan.tell(Subscribe {actor: sub.clone(), topic: "params".into()}, None);
        sensor_chan.tell(Subscribe {actor: sub.clone(), topic: "readings".into()}, None);


        // XXX a better way to get own reference?
        let props = Props::new_args(params::ParamWaiter::new_actor, (self.config.clone(), params_chan));
        ctx.actor_of(props, "paramwaiter").unwrap();

        if self.testmode {
            let props = Props::new_args(sensor::TestSensor::new_actor, (self.config.clone(), sensor_chan));
            ctx.actor_of(props, "sensor").unwrap()
        } else {
            let props = Props::new_args(sensor::OneWireSensor::new_actor, (self.config.clone(), sensor_chan));
            ctx.actor_of(props, "sensor").unwrap()
        };

        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, testmode, nowait) 
        : (Config, bool, bool)) -> Fridge {
        Self::new(config, testmode, nowait)

    }
    pub fn new(config: Config, 
            testmode: bool,
            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),
            testmode: testmode,
        };

        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;
        self.integrator.turn(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);
    }
}