Mercurial > templog
view rust/src/fridge.rs @ 638:a9f353f488d0 rust
fix channels
author | Matt Johnston <matt@ucc.asn.au> |
---|---|
date | Sat, 09 Nov 2019 11:35:59 +0800 |
parents | 43eb3cfdf769 |
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); } }