Compare commits

...

5 Commits

Author SHA1 Message Date
Nareshkumar Rao a3fa6d0599 wip 7 months ago
Nareshkumar Rao b0bdb6a975 wip 7 months ago
Nareshkumar Rao a6b050cc64 wip 7 months ago
Nareshkumar Rao 34e8aee41b wip control 7 months ago
Nareshkumar Rao 771f81a9f6 wip 7 months ago
  1. 22
      Cargo.lock
  2. 1
      Cargo.toml
  3. 12
      growpi.service
  4. 7
      growpi.toml
  5. 77
      html/growpi/src/App.vue
  6. 7
      html/growpi/src/main.js
  7. 16
      src/actuators.rs
  8. 23
      src/config.rs
  9. 64
      src/control.rs
  10. 30
      src/control/light.rs
  11. 21
      src/control/mod.rs
  12. 42
      src/control/soil.rs
  13. 38
      src/control/temperature.rs
  14. 49
      src/data_logging.rs
  15. 73
      src/history.rs
  16. 2
      src/main.rs
  17. 1
      src/server.rs
  18. 10
      src/state.rs

22
Cargo.lock

@ -207,6 +207,27 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "embedded-hal"
version = "0.2.7"
@ -333,6 +354,7 @@ dependencies = [
"ads1x1x",
"axum",
"chrono",
"csv",
"libc",
"nb 1.1.0",
"rppal",

1
Cargo.toml

@ -15,3 +15,4 @@ axum = { "version" = "0.7", features = ["macros"] }
tokio = { "version" = "1.37" }
chrono = "0.4"
tower-http = { "version" = "0.5", features = ["cors"] }
csv = "1.3.0"

12
growpi.service

@ -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

7
growpi.toml

@ -28,3 +28,10 @@ resistor = "R2"
[water_pump_settings]
grams_per_millisecond = 0.05280999839305878
[controller_settings]
temperature_set_point_upper = 35.0
temperature_set_point_lower = 28.0
temperature_loop_mins = 60
sunlight_hours = 24
soil_loop_hours = 12

77
html/growpi/src/App.vue

@ -1,5 +1,6 @@
<script>
import Button from 'primevue/button';
import InputNumber from 'primevue/inputnumber';
import ToggleButton from 'primevue/togglebutton';
export default {
data() {
@ -10,9 +11,10 @@ export default {
water_state: false,
water_primed: false,
fan_state: false,
pumpAmount: 150,
};
},
components: { ToggleButton, Button },
components: { ToggleButton, Button, InputNumber },
methods: {
async updateInfo() {
let info = await receiveInfo();
@ -21,15 +23,20 @@ export default {
this.pump_state = to_bool(info.pump_state);
this.temperature = info.temperature;
this.soil_moisture = info.soil_moisture;
console.log(info);
},
async sendSwitch(name, state) {
await fetch("http://192.168.0.107:2205/switch/" + name + "/" + to_state(state));
await this.updateInfo();
},
async pumpWater() {
console.log("Pumping: " + this.pumpAmount);
await fetch("http://192.168.0.107:2205/pump/" + this.pumpAmount);
await this.updateInfo();
}
},
created() {
setTimeout(this.updateInfo, 1000);
this.updateInfo();
setInterval(this.updateInfo, 1000);
}
}
@ -60,29 +67,75 @@ function to_bool(state) {
</script>
<template>
<h1>GrowPi!</h1>
<table>
<table class="main-table">
<tr>
<td>Temperature</td>
<td>
{{ Math.round(temperature * 100) / 100 }}°C
</td>
</tr>
<tr>
<td>Soil Moisture</td>
<td>
{{ Math.round(soil_moisture * 1000) / 10 }}%
</td>
</tr>
<tr>
<td>Lights</td>
<td>
<ToggleButton :modelValue="light_state" something="hi" @change="sendSwitch('lights', !light_state)" onLabel="On"
offLabel="Off" />
<ToggleButton class="whole-cell" :modelValue="light_state" something="hi"
@change="sendSwitch('lights', !light_state)" onLabel="On" offLabel="Off" />
</td>
</tr>
<tr>
<td>Fan</td>
<td>
<ToggleButton :modelValue="fan_state" @change="sendSwitch('fan', !fan_state)" onLabel="On" offLabel="Off" />
<ToggleButton class="whole-cell" :modelValue="fan_state" @change="sendSwitch('fan', !fan_state)" onLabel="On"
offLabel="Off" />
</td>
</tr>
<tr>
<td>Pump</td>
<td>
<ToggleButton v-model="water_primed" onLabel="Ready" offLabel="Click to Prime" />
<Button @change="sendSwitch('pump', !water_state); water_primed = false;" :disabled="!water_primed"
label="Pump Water" />
{{ water_state ? "Pump active" : "Pump inactive" }}
<div class="whole-cell">
<ToggleButton v-model="water_primed" onLabel="Ready" offLabel="Click to Prime" />
<Button @click="pumpWater(); water_primed = false;" :disabled="!water_primed && !water_state"
label="Pump Water" />
</div>
<br /><br />
<InputNumber class="whole-cell" v-model="pumpAmount" :allowEmpty="false" inputId="integeronly" suffix=" grams"
:min="100" :max="300" />
</td>
</tr>
</table>
</template>
<style></style>
<style>
html,
body {
height: 100%;
}
html {
display: table;
margin: auto;
}
body {
display: table-cell;
vertical-align: middle;
}
.main-table {
border-collapse: collapse;
}
.main-table td {
border-style: solid;
padding: 1em;
border-width: 1px;
}
.whole-cell {
width: 100%;
}
</style>

7
html/growpi/src/main.js

@ -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 PrimeVue from 'primevue/config'
import App from './App.vue'
createApp(App).mount('#app')
App.use(PrimeVue)
const app = createApp(App)
app.use(PrimeVue)
app.mount('#app')

16
src/actuators.rs

@ -1,6 +1,9 @@
use std::{thread, time::Duration};
use crate::{error::GenericResult, io::RelaySwitchState, state::ProgramState};
use crate::{
error::GenericResult, history::WateringRecord, io::RelaySwitchState, sensors,
state::ProgramState,
};
pub fn switch_lights(
state: RelaySwitchState,
@ -47,8 +50,19 @@ pub fn pump_water(water_mass_g: u16, program_state: &mut ProgramState) -> Generi
.grams_per_millisecond;
let duration_ms = duration_ms.round() as u64;
let duration = Duration::from_millis(duration_ms);
let moisture_before_watering = sensors::get_soil_moisture(&program_state.config)?;
switch_water_pump(RelaySwitchState::On, program_state)?;
thread::sleep(duration);
switch_water_pump(RelaySwitchState::Off, program_state)?;
program_state
.history
.watering_records
.push(WateringRecord::new(
water_mass_g.into(),
moisture_before_watering,
));
program_state.history.save()?;
Ok(())
}

23
src/config.rs

@ -44,6 +44,18 @@ pub struct BoardSettings {
pub logic_level: f32,
}
#[derive(Serialize, Deserialize)]
pub struct ControllerSettings {
pub temperature_set_point_upper: f32,
pub temperature_set_point_lower: f32,
pub temperature_loop_mins: u64,
pub sunlight_hours: u64,
pub soil_loop_hours: u64,
pub max_water_over_window_grams: u64,
pub max_water_window_hours: u64,
pub ideal_soil_moisture_pct: u64,
}
#[derive(Serialize, Deserialize)]
pub struct Configuration {
pub board_settings: BoardSettings,
@ -51,6 +63,7 @@ pub struct Configuration {
pub soil_moisture_settings: SoilMoistureSettings,
pub thermistor_settings: ThermistorSettings,
pub water_pump_settings: WaterPumpSettings,
pub controller_settings: ControllerSettings,
}
impl Configuration {
@ -93,6 +106,16 @@ impl Default for Configuration {
water_pump_settings: WaterPumpSettings {
grams_per_millisecond: 0.05281,
},
controller_settings: ControllerSettings {
temperature_set_point_upper: 35.,
temperature_set_point_lower: 28.,
temperature_loop_mins: 60,
soil_loop_hours: 12,
sunlight_hours: 24,
max_water_over_window_grams: 1000,
max_water_window_hours: 24,
ideal_soil_moisture_pct: 70,
},
}
}
}

64
src/control.rs

@ -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())
);
}

30
src/control/light.rs

@ -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;
}
}

21
src/control/mod.rs

@ -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())
);
}

42
src/control/soil.rs

@ -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(())
}

38
src/control/temperature.rs

@ -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;
}
}

49
src/data_logging.rs

@ -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(())
}
}

73
src/history.rs

@ -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();
}
}

2
src/main.rs

@ -10,7 +10,9 @@ mod actuators;
mod cli_mode;
mod config;
mod control;
mod data_logging;
mod error;
mod history;
mod io;
mod sensors;
mod server;

1
src/server.rs

@ -58,7 +58,6 @@ async fn switch_handler(
let mut program_state = program_state.lock().map_err(lock_err)?;
match device.as_str() {
"lights" => actuators::switch_lights(state, &mut program_state)?,
"pump" => actuators::switch_water_pump(state, &mut program_state)?,
"fan" => actuators::switch_fan(state, &mut program_state)?,
_ => (),
}

10
src/state.rs

@ -1,14 +1,20 @@
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 struct ProgramState {
pub config: Configuration,
pub relay: io::Relay,
pub history: History,
}
pub fn init_state(config: Configuration) -> GenericResult<ProgramStateShared> {
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…
Cancel
Save