Compare commits
5 Commits
c5cacb429d
...
a3fa6d0599
Author | SHA1 | Date |
---|---|---|
Nareshkumar Rao | a3fa6d0599 | 7 months ago |
Nareshkumar Rao | b0bdb6a975 | 7 months ago |
Nareshkumar Rao | a6b050cc64 | 7 months ago |
Nareshkumar Rao | 34e8aee41b | 7 months ago |
Nareshkumar Rao | 771f81a9f6 | 7 months ago |
18 changed files with 412 additions and 83 deletions
@ -0,0 +1,12 @@ |
|||||
|
[Unit] |
||||
|
Description=GrowPi Server and Control |
||||
|
After=network.target |
||||
|
|
||||
|
[Service] |
||||
|
Type=simple |
||||
|
Restart=always |
||||
|
RestartSec=10 |
||||
|
ExecStart=/opt/growpi/growpi |
||||
|
|
||||
|
[Install] |
||||
|
WantedBy=mutli-user.target |
@ -1,9 +1,10 @@ |
|||||
import 'primevue/resources/themes/aura-light-green/theme.css' |
|
||||
|
import 'primevue/resources/themes/aura-dark-green/theme.css' |
||||
|
|
||||
import { createApp } from 'vue' |
import { createApp } from 'vue' |
||||
import PrimeVue from 'primevue/config' |
import PrimeVue from 'primevue/config' |
||||
import App from './App.vue' |
import App from './App.vue' |
||||
|
|
||||
createApp(App).mount('#app') |
|
||||
App.use(PrimeVue) |
|
||||
|
const app = createApp(App) |
||||
|
app.use(PrimeVue) |
||||
|
app.mount('#app') |
||||
|
|
||||
|
@ -1,64 +0,0 @@ |
|||||
use std::time::Duration; |
|
||||
|
|
||||
use chrono::{Local, Timelike}; |
|
||||
use tokio::join; |
|
||||
|
|
||||
use crate::{ |
|
||||
actuators, |
|
||||
error::{lock_err, GenericResult}, |
|
||||
sensors, |
|
||||
state::ProgramStateShared, |
|
||||
}; |
|
||||
|
|
||||
async fn temperature_control(program_state: ProgramStateShared) -> GenericResult<()> { |
|
||||
let mut program_state = program_state.lock().map_err(lock_err)?; |
|
||||
let current_temperature = sensors::get_temperature(&program_state.config)?; |
|
||||
if current_temperature > 28. { |
|
||||
actuators::switch_fan(crate::io::RelaySwitchState::On, &mut program_state)?; |
|
||||
} else { |
|
||||
actuators::switch_fan(crate::io::RelaySwitchState::Off, &mut program_state)?; |
|
||||
} |
|
||||
Ok(()) |
|
||||
} |
|
||||
|
|
||||
async fn temperature_control_loop(program_state: ProgramStateShared) { |
|
||||
loop { |
|
||||
let _ = temperature_control(program_state.clone()).await; |
|
||||
tokio::time::sleep(Duration::from_mins(1)).await; |
|
||||
} |
|
||||
} |
|
||||
async fn soil_moisture_control_loop(program_state: ProgramStateShared) { |
|
||||
loop { |
|
||||
tokio::time::sleep(Duration::from_days(1)).await; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async fn light_control(program_state: ProgramStateShared) -> GenericResult<()> { |
|
||||
let program_state = program_state.clone(); |
|
||||
let mut program_state = program_state.lock().map_err(lock_err)?; |
|
||||
let local = Local::now(); |
|
||||
let hour = local.time().hour(); |
|
||||
const HOURS_ON: u32 = 24; |
|
||||
if hour <= HOURS_ON { |
|
||||
actuators::switch_lights(crate::io::RelaySwitchState::On, &mut program_state)?; |
|
||||
} else { |
|
||||
actuators::switch_lights(crate::io::RelaySwitchState::Off, &mut program_state)?; |
|
||||
} |
|
||||
|
|
||||
Ok(()) |
|
||||
} |
|
||||
|
|
||||
async fn light_control_loop(program_state: ProgramStateShared) { |
|
||||
loop { |
|
||||
let _ = light_control(program_state.clone()).await; |
|
||||
tokio::time::sleep(Duration::from_hours(1)).await; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
pub async fn control_thread(program_state: ProgramStateShared) { |
|
||||
join!( |
|
||||
light_control_loop(program_state.clone()), |
|
||||
temperature_control_loop(program_state.clone()), |
|
||||
soil_moisture_control_loop(program_state.clone()) |
|
||||
); |
|
||||
} |
|
@ -0,0 +1,30 @@ |
|||||
|
use std::time::Duration; |
||||
|
|
||||
|
use chrono::{Local, Timelike}; |
||||
|
|
||||
|
use crate::{ |
||||
|
actuators, |
||||
|
error::{lock_err, GenericResult}, |
||||
|
state::ProgramStateShared, |
||||
|
}; |
||||
|
|
||||
|
fn light_control(program_state: ProgramStateShared) -> GenericResult<()> { |
||||
|
let program_state = program_state.clone(); |
||||
|
let mut program_state = program_state.lock().map_err(lock_err)?; |
||||
|
let local = Local::now(); |
||||
|
let hour = local.time().hour(); |
||||
|
if hour as u64 <= program_state.config.controller_settings.sunlight_hours { |
||||
|
actuators::switch_lights(crate::io::RelaySwitchState::On, &mut program_state)?; |
||||
|
} else { |
||||
|
actuators::switch_lights(crate::io::RelaySwitchState::Off, &mut program_state)?; |
||||
|
} |
||||
|
|
||||
|
Ok(()) |
||||
|
} |
||||
|
|
||||
|
pub async fn light_control_loop(program_state: ProgramStateShared) { |
||||
|
loop { |
||||
|
let _ = light_control(program_state.clone()); |
||||
|
tokio::time::sleep(Duration::from_hours(1)).await; |
||||
|
} |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
use tokio::join; |
||||
|
|
||||
|
use crate::{ |
||||
|
control::{ |
||||
|
light::light_control_loop, soil::soil_moisture_control_loop, |
||||
|
temperature::temperature_control_loop, |
||||
|
}, |
||||
|
state::ProgramStateShared, |
||||
|
}; |
||||
|
|
||||
|
mod light; |
||||
|
mod soil; |
||||
|
mod temperature; |
||||
|
|
||||
|
pub async fn control_thread(program_state: ProgramStateShared) { |
||||
|
join!( |
||||
|
light_control_loop(program_state.clone()), |
||||
|
temperature_control_loop(program_state.clone()), |
||||
|
soil_moisture_control_loop(program_state.clone()) |
||||
|
); |
||||
|
} |
@ -0,0 +1,42 @@ |
|||||
|
use std::time::Duration; |
||||
|
|
||||
|
use chrono::{DateTime, Utc}; |
||||
|
|
||||
|
use crate::{ |
||||
|
error::{lock_err, GenericResult}, |
||||
|
state::ProgramStateShared, |
||||
|
}; |
||||
|
|
||||
|
pub async fn soil_moisture_control_loop(program_state: ProgramStateShared) { |
||||
|
let loop_duration = program_state |
||||
|
.lock() |
||||
|
.map(|program_state| program_state.config.controller_settings.soil_loop_hours) |
||||
|
.unwrap_or(1); |
||||
|
|
||||
|
loop { |
||||
|
let _ = soil_moisture_control(program_state.clone()); |
||||
|
tokio::time::sleep(Duration::from_hours(loop_duration)).await; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fn soil_moisture_control(program_state: ProgramStateShared) -> GenericResult<()> { |
||||
|
let program_state = program_state.lock().map_err(lock_err)?; |
||||
|
let config = &program_state.config.controller_settings; |
||||
|
let history = &program_state.history.watering_records; |
||||
|
|
||||
|
let water_amount_over_window: u64 = history |
||||
|
.iter() |
||||
|
.filter(|record| { |
||||
|
if let Some(time) = DateTime::from_timestamp(record.time, 0) { |
||||
|
let delta = Utc::now() - time; |
||||
|
let delta = delta.num_hours(); |
||||
|
delta <= config.max_water_window_hours.try_into().unwrap_or(24) |
||||
|
} else { |
||||
|
false |
||||
|
} |
||||
|
}) |
||||
|
.map(|record| record.amount) |
||||
|
.sum(); |
||||
|
|
||||
|
Ok(()) |
||||
|
} |
@ -0,0 +1,38 @@ |
|||||
|
use std::time::Duration; |
||||
|
|
||||
|
use crate::{ |
||||
|
actuators, |
||||
|
error::{lock_err, GenericResult}, |
||||
|
sensors, |
||||
|
state::ProgramStateShared, |
||||
|
}; |
||||
|
|
||||
|
fn temperature_control(program_state: ProgramStateShared) -> GenericResult<()> { |
||||
|
let mut program_state = program_state.lock().map_err(lock_err)?; |
||||
|
let config = &program_state.config.controller_settings; |
||||
|
|
||||
|
let current_temperature = sensors::get_temperature(&program_state.config)?; |
||||
|
if current_temperature > config.temperature_set_point_upper { |
||||
|
actuators::switch_fan(crate::io::RelaySwitchState::On, &mut program_state)?; |
||||
|
} else if current_temperature < config.temperature_set_point_lower { |
||||
|
actuators::switch_fan(crate::io::RelaySwitchState::Off, &mut program_state)?; |
||||
|
} |
||||
|
Ok(()) |
||||
|
} |
||||
|
|
||||
|
pub async fn temperature_control_loop(program_state: ProgramStateShared) { |
||||
|
let loop_duration = program_state |
||||
|
.lock() |
||||
|
.map(|program_state| { |
||||
|
program_state |
||||
|
.config |
||||
|
.controller_settings |
||||
|
.temperature_loop_mins |
||||
|
}) |
||||
|
.unwrap_or(1); |
||||
|
|
||||
|
loop { |
||||
|
let _ = temperature_control(program_state.clone()); |
||||
|
tokio::time::sleep(Duration::from_mins(loop_duration)).await; |
||||
|
} |
||||
|
} |
@ -0,0 +1,49 @@ |
|||||
|
use chrono::Utc; |
||||
|
use serde::{Deserialize, Serialize}; |
||||
|
|
||||
|
use crate::{ |
||||
|
error::{lock_err, GenericResult}, |
||||
|
sensors, |
||||
|
state::ProgramStateShared, |
||||
|
}; |
||||
|
|
||||
|
#[derive(Serialize, Deserialize)] |
||||
|
pub struct DataRecord { |
||||
|
pub timestamp: i64, |
||||
|
pub temperature: f32, |
||||
|
pub soil_mositure: f32, |
||||
|
} |
||||
|
|
||||
|
#[derive(Serialize, Deserialize)] |
||||
|
pub struct DataRecords { |
||||
|
pub records: Vec<DataRecord>, |
||||
|
} |
||||
|
|
||||
|
const FILE_PATH: &str = "./growpi.datalog.csv"; |
||||
|
impl DataRecords { |
||||
|
fn load() -> GenericResult<DataRecords> { |
||||
|
let mut data = csv::ReaderBuilder::new() |
||||
|
.has_headers(true) |
||||
|
.from_path(FILE_PATH)?; |
||||
|
let mut result = Vec::new(); |
||||
|
for record in data.deserialize() { |
||||
|
result.push(record?); |
||||
|
} |
||||
|
Ok(DataRecords { records: result }) |
||||
|
} |
||||
|
pub fn push(program_state: ProgramStateShared) -> GenericResult<()> { |
||||
|
let program_state = program_state.lock().map_err(lock_err)?; |
||||
|
let config = &program_state.config; |
||||
|
let record = DataRecord { |
||||
|
timestamp: Utc::now().timestamp(), |
||||
|
temperature: sensors::get_temperature(config)?, |
||||
|
soil_mositure: sensors::get_soil_moisture(config)?, |
||||
|
}; |
||||
|
let mut writer = csv::WriterBuilder::new() |
||||
|
.has_headers(true) |
||||
|
.from_path(FILE_PATH)?; |
||||
|
writer.serialize(record)?; |
||||
|
writer.flush()?; |
||||
|
Ok(()) |
||||
|
} |
||||
|
} |
@ -0,0 +1,73 @@ |
|||||
|
use chrono::Utc; |
||||
|
use serde::{Deserialize, Serialize}; |
||||
|
|
||||
|
use crate::error::GenericResult; |
||||
|
|
||||
|
#[derive(Serialize, Deserialize)] |
||||
|
pub struct WateringRecord { |
||||
|
pub time: i64, |
||||
|
pub amount: u64, |
||||
|
pub moisture_before_watering: f32, |
||||
|
} |
||||
|
|
||||
|
impl WateringRecord { |
||||
|
pub fn new(amount: u64, moisture_before_watering: f32) -> WateringRecord { |
||||
|
WateringRecord { |
||||
|
time: Utc::now().timestamp(), |
||||
|
amount, |
||||
|
moisture_before_watering, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#[derive(Default)] |
||||
|
pub struct History { |
||||
|
pub watering_records: Vec<WateringRecord>, |
||||
|
} |
||||
|
|
||||
|
const FILE_PATH: &str = "./growpi.history.csv"; |
||||
|
|
||||
|
impl History { |
||||
|
pub fn save(&self) -> GenericResult<()> { |
||||
|
let mut writer = csv::WriterBuilder::new() |
||||
|
.has_headers(true) |
||||
|
.from_path(FILE_PATH)?; |
||||
|
for record in &self.watering_records { |
||||
|
writer.serialize(record)?; |
||||
|
} |
||||
|
writer.flush()?; |
||||
|
Ok(()) |
||||
|
} |
||||
|
|
||||
|
pub fn load() -> GenericResult<History> { |
||||
|
let mut history = csv::ReaderBuilder::new() |
||||
|
.has_headers(true) |
||||
|
.from_path(FILE_PATH)?; |
||||
|
let mut result = Vec::new(); |
||||
|
for record in history.deserialize() { |
||||
|
result.push(record?); |
||||
|
} |
||||
|
Ok(History { |
||||
|
watering_records: result, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#[cfg(test)] |
||||
|
mod tests { |
||||
|
|
||||
|
use chrono::Local; |
||||
|
|
||||
|
use super::*; |
||||
|
|
||||
|
#[test] |
||||
|
fn test_write_default() { |
||||
|
let mut history = History::default(); |
||||
|
history.watering_records.push(WateringRecord { |
||||
|
time: Local::now().timestamp(), |
||||
|
amount: 456, |
||||
|
moisture_before_watering: 71.1, |
||||
|
}); |
||||
|
history.save().unwrap(); |
||||
|
} |
||||
|
} |
@ -1,14 +1,20 @@ |
|||||
use std::sync::{Arc, Mutex}; |
use std::sync::{Arc, Mutex}; |
||||
|
|
||||
use crate::{config::Configuration, error::GenericResult, io}; |
|
||||
|
use crate::{config::Configuration, error::GenericResult, history::History, io}; |
||||
|
|
||||
pub type ProgramStateShared = Arc<Mutex<ProgramState>>; |
pub type ProgramStateShared = Arc<Mutex<ProgramState>>; |
||||
pub struct ProgramState { |
pub struct ProgramState { |
||||
pub config: Configuration, |
pub config: Configuration, |
||||
pub relay: io::Relay, |
pub relay: io::Relay, |
||||
|
pub history: History, |
||||
} |
} |
||||
|
|
||||
pub fn init_state(config: Configuration) -> GenericResult<ProgramStateShared> { |
pub fn init_state(config: Configuration) -> GenericResult<ProgramStateShared> { |
||||
let relay = io::Relay::new(&config)?; |
let relay = io::Relay::new(&config)?; |
||||
Ok(Arc::new(Mutex::new(ProgramState { config, relay }))) |
|
||||
|
let history = History::load().unwrap_or_default(); |
||||
|
Ok(Arc::new(Mutex::new(ProgramState { |
||||
|
config, |
||||
|
relay, |
||||
|
history, |
||||
|
}))) |
||||
} |
} |
||||
|
Loading…
Reference in new issue