Compare commits
10 Commits
a30dbe0759
...
680d1c3aaf
Author | SHA1 | Date | |
---|---|---|---|
680d1c3aaf | |||
060a1cc32d | |||
541f7e4471 | |||
7ea1486e2c | |||
b933516062 | |||
b533739aa4 | |||
12463c557b | |||
9473466feb | |||
b5b5b25238 | |||
b9ec3247af |
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,5 +8,5 @@ target
|
|||||||
Cargo.lock
|
Cargo.lock
|
||||||
node_modules/
|
node_modules/
|
||||||
rust/src/webserver/bundle.js
|
rust/src/webserver/bundle.js
|
||||||
|
rust/build/
|
||||||
rust/image.bin
|
rust/image.bin
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"board": {
|
"board": {
|
||||||
"active_layer": 0,
|
"active_layer": 2,
|
||||||
"active_layer_preset": "",
|
"active_layer_preset": "",
|
||||||
"auto_track_width": false,
|
"auto_track_width": false,
|
||||||
"hidden_netclasses": [],
|
"hidden_netclasses": [],
|
||||||
|
@ -55,29 +55,7 @@
|
|||||||
"width": 0.0
|
"width": 0.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"drc_exclusions": [
|
"drc_exclusions": [],
|
||||||
"courtyards_overlap|189080001|128865001|67ac55df-4a92-42f8-90ae-0f317d1fcee5|c62213e3-5f38-460c-825e-5567e84c0a13",
|
|
||||||
"courtyards_overlap|199080001|128865001|67ac55df-4a92-42f8-90ae-0f317d1fcee5|cfde0667-9234-45fc-8eda-14a18003cf8d",
|
|
||||||
"courtyards_overlap|209080001|128865001|5ed14a34-184f-41cd-94a0-c9f4df9ff6fd|cfde0667-9234-45fc-8eda-14a18003cf8d",
|
|
||||||
"courtyards_overlap|219080001|128865001|4dcef43e-4bc3-40ed-bbe5-22243d3d487f|5ed14a34-184f-41cd-94a0-c9f4df9ff6fd",
|
|
||||||
"courtyards_overlap|229080001|128865001|1c5c2a00-3616-406b-aedf-bf3a81986791|4dcef43e-4bc3-40ed-bbe5-22243d3d487f",
|
|
||||||
"courtyards_overlap|239080001|128865001|1c5c2a00-3616-406b-aedf-bf3a81986791|1ecf7cd6-6488-4305-b20b-72dbab800538",
|
|
||||||
"courtyards_overlap|249080001|128865001|1ecf7cd6-6488-4305-b20b-72dbab800538|6ccc9c73-6edf-479a-97a6-a01eda77987f",
|
|
||||||
"courtyards_overlap|268679999|80770001|22510631-57e1-46b7-916a-63be4d1f1249|ab413b79-2f68-48c4-8ed3-cedb5f870570",
|
|
||||||
"silk_edge_clearance|170180000|117670000|87e2c53a-11a1-4d9d-89a9-2a5ca6e6f269|058105c1-5307-4874-9401-d1e0612824b0",
|
|
||||||
"silk_edge_clearance|170180000|49705000|87e2c53a-11a1-4d9d-89a9-2a5ca6e6f269|2af3bc4a-e24d-48a4-898d-739774f84bd6",
|
|
||||||
"silk_edge_clearance|170180000|67945000|87e2c53a-11a1-4d9d-89a9-2a5ca6e6f269|d1ed732d-93f0-4bca-ae40-dde82301c749",
|
|
||||||
"silk_edge_clearance|170242295|123498571|87e2c53a-11a1-4d9d-89a9-2a5ca6e6f269|98242ad4-5a51-4508-b392-eeabff738172",
|
|
||||||
"silk_edge_clearance|170242295|127998571|87e2c53a-11a1-4d9d-89a9-2a5ca6e6f269|9dfe510c-725d-437b-9270-62f0fb83346b",
|
|
||||||
"silk_edge_clearance|269240000|110840000|6d070ea4-5732-4974-b15a-6adc25e87507|ab851b96-316a-460d-b1e8-9d3fef419e7b",
|
|
||||||
"silk_edge_clearance|269240000|113440000|6d070ea4-5732-4974-b15a-6adc25e87507|f1ae4a26-c73e-4b69-af96-801c2a8e61d3",
|
|
||||||
"silk_edge_clearance|269240000|115980000|6d070ea4-5732-4974-b15a-6adc25e87507|685b88f7-a226-4ce5-b9a2-70f5eea54d45",
|
|
||||||
"silk_edge_clearance|269240000|118520000|6d070ea4-5732-4974-b15a-6adc25e87507|dfd6db18-258f-4809-8096-700f3184e482",
|
|
||||||
"silk_edge_clearance|269240000|121060000|6d070ea4-5732-4974-b15a-6adc25e87507|df12db4f-f2d6-4bbb-962f-c504c70a12d7",
|
|
||||||
"silk_edge_clearance|269240000|123600000|6d070ea4-5732-4974-b15a-6adc25e87507|0f1bb1cf-595c-42a5-b79f-53ba7db0e565",
|
|
||||||
"silk_edge_clearance|269240000|126140000|6d070ea4-5732-4974-b15a-6adc25e87507|85c77ea0-2469-4305-9635-0c4ccb268a57",
|
|
||||||
"silk_edge_clearance|269240000|128740000|6d070ea4-5732-4974-b15a-6adc25e87507|90543f7d-a988-4bce-a183-20300704c3c1"
|
|
||||||
],
|
|
||||||
"meta": {
|
"meta": {
|
||||||
"filename": "board_design_settings.json",
|
"filename": "board_design_settings.json",
|
||||||
"version": 2
|
"version": 2
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@ target = "xtensa-esp32-espidf"
|
|||||||
|
|
||||||
[target.xtensa-esp32-espidf]
|
[target.xtensa-esp32-espidf]
|
||||||
linker = "ldproxy"
|
linker = "ldproxy"
|
||||||
|
#runner = "espflash flash --monitor --partition-table partitions.csv" # Select this runner for espflash v2.x.x
|
||||||
runner = "espflash flash --monitor --baud 921600 --partition-table partitions.csv" # Select this runner for espflash v2.x.x
|
runner = "espflash flash --monitor --baud 921600 --partition-table partitions.csv" # Select this runner for espflash v2.x.x
|
||||||
#runner = "cargo runner"
|
#runner = "cargo runner"
|
||||||
rustflags = [ "--cfg", "espidf_time64"] # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110
|
rustflags = [ "--cfg", "espidf_time64"] # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110
|
||||||
|
@ -55,29 +55,28 @@ embassy = ["esp-idf-svc/embassy-sync", "esp-idf-svc/critical-section", "esp-idf-
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = { version = "0.4", default-features = false }
|
log = { version = "0.4", default-features = false }
|
||||||
esp-idf-svc = { version = "0.47.3", default-features = false }
|
esp-idf-svc = { version = "0.48.0", default-features = false }
|
||||||
serde = { version = "1.0.192", features = ["derive"] }
|
serde = { version = "1.0.192", features = ["derive"] }
|
||||||
average = { version = "0.14.1" , features = ["std"] }
|
average = { version = "0.14.1" , features = ["std"] }
|
||||||
#esp32 = "0.28.0"
|
#esp32 = "0.28.0"
|
||||||
bit_field = "0.10.2"
|
bit_field = "0.10.2"
|
||||||
ds18b20 = "0.1.1"
|
ds18b20 = "0.1.1"
|
||||||
embedded-svc = { version = "0.26.4", features = ["experimental"] }
|
embedded-svc = { version = "0.27.0", features = ["experimental"] }
|
||||||
esp-idf-hal = "0.42.5"
|
esp-idf-hal = "0.43.0"
|
||||||
esp-idf-sys = { version = "0.33.7", features = ["binstart", "native"] }
|
esp-idf-sys = { version = "0.34.0", features = ["binstart", "native"] }
|
||||||
esp-ota = "0.2.0"
|
|
||||||
esp_idf_build = "0.1.3"
|
esp_idf_build = "0.1.3"
|
||||||
chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone"] }
|
chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone"] }
|
||||||
chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]}
|
chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]}
|
||||||
embedded-hal = "0.2.7"
|
embedded-hal = "1.0.0"
|
||||||
#shift-register-driver = "0.1.1"
|
|
||||||
one-wire-bus = "0.1.1"
|
one-wire-bus = "0.1.1"
|
||||||
anyhow = { version = "1.0.75", features = ["std", "backtrace"] }
|
anyhow = { version = "1.0.75", features = ["std", "backtrace"] }
|
||||||
schemars = "0.8.16"
|
schemars = "0.8.16"
|
||||||
heapless = { version = "0.7", features = ["serde"] }
|
heapless = { version = "0.8", features = ["serde"] }
|
||||||
serde_json = "1.0.108"
|
serde_json = "1.0.108"
|
||||||
strum = { version = "0.25.0", features = ["derive"] }
|
strum = { version = "0.26.1", features = ["derive"] }
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
#?bq34z100 required
|
measurements = "0.11.0"
|
||||||
|
bq34z100 = "0.1.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
embuild = "0.31.3"
|
embuild = "0.31.3"
|
||||||
|
@ -5,6 +5,4 @@ CONFIG_ESP_MAIN_TASK_STACK_SIZE=20000
|
|||||||
# This allows to use 1 ms granuality for thread sleeps (10 ms by default).
|
# This allows to use 1 ms granuality for thread sleeps (10 ms by default).
|
||||||
CONFIG_FREERTOS_HZ=1000
|
CONFIG_FREERTOS_HZ=1000
|
||||||
|
|
||||||
# Workaround for https://github.com/espressif/esp-idf/issues/7631
|
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=true
|
||||||
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
|
|
||||||
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n
|
|
@ -1,4 +1,4 @@
|
|||||||
use std::fmt;
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -14,8 +14,8 @@ pub struct Config {
|
|||||||
pub tank_sensor_enabled: bool,
|
pub tank_sensor_enabled: bool,
|
||||||
pub tank_useable_ml: u32,
|
pub tank_useable_ml: u32,
|
||||||
pub tank_warn_percent: u8,
|
pub tank_warn_percent: u8,
|
||||||
pub tank_empty_mv: f32,
|
pub tank_empty_percent: u8,
|
||||||
pub tank_full_mv: f32,
|
pub tank_full_percent: u8,
|
||||||
|
|
||||||
pub night_lamp_hour_start: u8,
|
pub night_lamp_hour_start: u8,
|
||||||
pub night_lamp_hour_end: u8,
|
pub night_lamp_hour_end: u8,
|
||||||
@ -27,8 +27,8 @@ pub struct Config {
|
|||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
base_topic: "plant/one".into(),
|
base_topic: heapless::String::from_str("plant/one").unwrap(),
|
||||||
mqtt_url: "mqtt://192.168.1.1:1883".into(),
|
mqtt_url: heapless::String::from_str("mqtt://192.168.1.1:1883").unwrap(),
|
||||||
tank_allow_pumping_if_sensor_error: true,
|
tank_allow_pumping_if_sensor_error: true,
|
||||||
tank_sensor_enabled: true,
|
tank_sensor_enabled: true,
|
||||||
tank_warn_percent: 50,
|
tank_warn_percent: 50,
|
||||||
@ -38,8 +38,8 @@ impl Default for Config {
|
|||||||
plants: [Plant::default(); PLANT_COUNT],
|
plants: [Plant::default(); PLANT_COUNT],
|
||||||
max_consecutive_pump_count: 15,
|
max_consecutive_pump_count: 15,
|
||||||
tank_useable_ml: 5000,
|
tank_useable_ml: 5000,
|
||||||
tank_empty_mv: 0.1,
|
tank_empty_percent: 0_u8,
|
||||||
tank_full_mv: 3.3,
|
tank_full_percent: 100_u8,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,6 +48,7 @@ pub enum Mode {
|
|||||||
OFF,
|
OFF,
|
||||||
TargetMoisture,
|
TargetMoisture,
|
||||||
TimerOnly,
|
TimerOnly,
|
||||||
|
TimerAndDeadzone,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
|
||||||
|
266
rust/src/espota.rs
Normal file
266
rust/src/espota.rs
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
use core::fmt;
|
||||||
|
use core::mem;
|
||||||
|
use core::ptr;
|
||||||
|
use esp_idf_sys::{
|
||||||
|
esp_ota_abort, esp_ota_begin, esp_ota_end, esp_ota_get_next_update_partition, esp_ota_handle_t,
|
||||||
|
esp_ota_mark_app_invalid_rollback_and_reboot, esp_ota_mark_app_valid_cancel_rollback,
|
||||||
|
esp_ota_set_boot_partition, esp_ota_write, esp_partition_t, esp_restart, ESP_ERR_FLASH_OP_FAIL,
|
||||||
|
ESP_ERR_FLASH_OP_TIMEOUT, ESP_ERR_INVALID_ARG, ESP_ERR_INVALID_SIZE, ESP_ERR_INVALID_STATE,
|
||||||
|
ESP_ERR_NOT_FOUND, ESP_ERR_NO_MEM, ESP_ERR_OTA_PARTITION_CONFLICT, ESP_ERR_OTA_ROLLBACK_FAILED,
|
||||||
|
ESP_ERR_OTA_ROLLBACK_INVALID_STATE, ESP_ERR_OTA_SELECT_INFO_INVALID,
|
||||||
|
ESP_ERR_OTA_VALIDATE_FAILED, ESP_FAIL, ESP_OK, OTA_SIZE_UNKNOWN,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
|
/// An error that can happen during ESP OTA operations.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Error {
|
||||||
|
kind: ErrorKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub(crate) fn from_kind(kind: ErrorKind) -> Self {
|
||||||
|
Self { kind }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the kind of error as an enum, that can be matched on.
|
||||||
|
pub fn kind(&self) -> ErrorKind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.kind.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ErrorKind {
|
||||||
|
/// No suitable partition for writing OTA update to found.
|
||||||
|
NoOtaPartition,
|
||||||
|
/// Cannot allocate memory for OTA operation.
|
||||||
|
AllocFailed,
|
||||||
|
/// Rollback enabled, but the currently running application is still pending. The currently
|
||||||
|
/// running application must confirm itself before downloading and flashing a new app.
|
||||||
|
InvalidRollbackState,
|
||||||
|
/// First byte of image contains invalid app image magic byte.
|
||||||
|
InvalidMagicByte,
|
||||||
|
/// Flash write operation timed out.
|
||||||
|
FlashTimeout,
|
||||||
|
/// Flash write operation failed.
|
||||||
|
FlashFailed,
|
||||||
|
/// OTA data partition has invalid contents.
|
||||||
|
InvalidOtaPartitionData,
|
||||||
|
/// The [`OtaUpdate`] handle was finalized before any app image was written to it.
|
||||||
|
NothingWritten,
|
||||||
|
/// OTA image is invalid (either not a valid app image, or - if secure boot is enabled - signature failed to verify.)
|
||||||
|
InvalidImage,
|
||||||
|
/// If flash encryption is enabled, this result indicates an internal error writing the final encrypted bytes to flash.
|
||||||
|
WritingEncryptedFailed,
|
||||||
|
/// The rollback failed.
|
||||||
|
RollbackFailed,
|
||||||
|
/// The rollback is not possible due to flash does not have any apps.
|
||||||
|
RollbackFailedNoApps,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ErrorKind {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
use ErrorKind::*;
|
||||||
|
match self {
|
||||||
|
NoOtaPartition => "No suitable partition for writing OTA update to found",
|
||||||
|
AllocFailed => "Cannot allocate memory for OTA operation",
|
||||||
|
InvalidRollbackState => {
|
||||||
|
"Rollback enabled, but the currently running application is still pending"
|
||||||
|
}
|
||||||
|
InvalidMagicByte => "First byte of image contains invalid app image magic byte",
|
||||||
|
FlashTimeout => "Flash write operation timed out",
|
||||||
|
FlashFailed => "Flash write operation failed",
|
||||||
|
InvalidOtaPartitionData => "OTA data partition has invalid contents",
|
||||||
|
NothingWritten => "OtaUpdate was never written to",
|
||||||
|
InvalidImage => "OTA image is invalid",
|
||||||
|
WritingEncryptedFailed => "Internal error writing the final encrypted bytes to flash",
|
||||||
|
RollbackFailed => "The rollback failed",
|
||||||
|
RollbackFailedNoApps => {
|
||||||
|
"The rollback is not possible due to flash does not have any apps"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents an ongoing OTA update.
|
||||||
|
///
|
||||||
|
/// Dropping this object before calling [`finalize`](OtaUpdate::finalize) will abort the update.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OtaUpdate {
|
||||||
|
partition: *const esp_partition_t,
|
||||||
|
ota_handle: esp_ota_handle_t,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OtaUpdate {
|
||||||
|
/// Starts an OTA update to the next OTA compatible partition.
|
||||||
|
///
|
||||||
|
/// Finds next partition round-robin, starting from the current running partition.
|
||||||
|
/// The entire partition is erased.
|
||||||
|
pub fn begin() -> Result<Self> {
|
||||||
|
let partition = unsafe { esp_ota_get_next_update_partition(ptr::null()) };
|
||||||
|
if partition.is_null() {
|
||||||
|
return Err(Error::from_kind(ErrorKind::NoOtaPartition));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ota_handle = 0;
|
||||||
|
match unsafe { esp_ota_begin(partition, OTA_SIZE_UNKNOWN as usize, &mut ota_handle) } {
|
||||||
|
ESP_OK => Ok(()),
|
||||||
|
ESP_ERR_INVALID_ARG => panic!("Invalid partition or out_handle"),
|
||||||
|
ESP_ERR_NO_MEM => Err(Error::from_kind(ErrorKind::AllocFailed)),
|
||||||
|
ESP_ERR_OTA_PARTITION_CONFLICT => Err(Error::from_kind(ErrorKind::NoOtaPartition)),
|
||||||
|
ESP_ERR_NOT_FOUND => panic!("Partition argument not found in partition table"),
|
||||||
|
ESP_ERR_OTA_SELECT_INFO_INVALID => {
|
||||||
|
Err(Error::from_kind(ErrorKind::InvalidOtaPartitionData))
|
||||||
|
}
|
||||||
|
ESP_ERR_INVALID_SIZE => panic!("Partition doesn’t fit in configured flash size"),
|
||||||
|
ESP_ERR_FLASH_OP_TIMEOUT => Err(Error::from_kind(ErrorKind::FlashTimeout)),
|
||||||
|
ESP_ERR_FLASH_OP_FAIL => Err(Error::from_kind(ErrorKind::FlashFailed)),
|
||||||
|
ESP_ERR_OTA_ROLLBACK_INVALID_STATE => {
|
||||||
|
Err(Error::from_kind(ErrorKind::InvalidRollbackState))
|
||||||
|
}
|
||||||
|
code => panic!("Unexpected esp_ota_begin return code: {}", code),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
partition,
|
||||||
|
ota_handle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write app image data to partition.
|
||||||
|
///
|
||||||
|
/// This method can be called multiple times as data is received during the OTA operation.
|
||||||
|
/// Data is written sequentially to the partition.
|
||||||
|
///
|
||||||
|
/// The format of the app image can be read about in the main README and crate documentation.
|
||||||
|
pub fn write(&mut self, app_image_chunk: &[u8]) -> Result<()> {
|
||||||
|
let chunk_ptr = app_image_chunk.as_ptr() as *const _;
|
||||||
|
let chunk_len = app_image_chunk.len();
|
||||||
|
|
||||||
|
match unsafe { esp_ota_write(self.ota_handle, chunk_ptr, chunk_len) } {
|
||||||
|
ESP_OK => Ok(()),
|
||||||
|
ESP_ERR_INVALID_ARG => panic!("Invalid OTA handle"),
|
||||||
|
ESP_ERR_OTA_VALIDATE_FAILED => Err(Error::from_kind(ErrorKind::InvalidMagicByte)),
|
||||||
|
ESP_ERR_FLASH_OP_TIMEOUT => Err(Error::from_kind(ErrorKind::FlashTimeout)),
|
||||||
|
ESP_ERR_FLASH_OP_FAIL => Err(Error::from_kind(ErrorKind::FlashFailed)),
|
||||||
|
ESP_ERR_OTA_SELECT_INFO_INVALID => {
|
||||||
|
Err(Error::from_kind(ErrorKind::InvalidOtaPartitionData))
|
||||||
|
}
|
||||||
|
code => panic!("Unexpected esp_ota_write return code: {code}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finish OTA update and validate newly written app image.
|
||||||
|
///
|
||||||
|
/// Unless you also call [`set_as_boot_partition`](CompletedOtaUpdate::set_as_boot_partition) the new app will not
|
||||||
|
/// start.
|
||||||
|
pub fn finalize(self) -> Result<CompletedOtaUpdate> {
|
||||||
|
match unsafe { esp_ota_end(self.ota_handle) } {
|
||||||
|
ESP_OK => Ok(()),
|
||||||
|
ESP_ERR_NOT_FOUND => panic!("Invalid OTA handle"),
|
||||||
|
ESP_ERR_INVALID_ARG => Err(Error::from_kind(ErrorKind::NothingWritten)),
|
||||||
|
ESP_ERR_OTA_VALIDATE_FAILED => Err(Error::from_kind(ErrorKind::InvalidImage)),
|
||||||
|
ESP_ERR_INVALID_STATE => Err(Error::from_kind(ErrorKind::WritingEncryptedFailed)),
|
||||||
|
code => panic!("Unexpected esp_ota_end return code: {code}"),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let partition = self.partition;
|
||||||
|
mem::forget(self);
|
||||||
|
|
||||||
|
Ok(CompletedOtaUpdate { partition })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a raw pointer to the partition that the new app is/will be written to.
|
||||||
|
pub fn raw_partition(&self) -> *const esp_partition_t {
|
||||||
|
self.partition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for OtaUpdate {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
log::debug!("Aborting OTA update");
|
||||||
|
|
||||||
|
let abort_result_code = unsafe { esp_ota_abort(self.ota_handle) };
|
||||||
|
if abort_result_code != ESP_OK {
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
log::error!(
|
||||||
|
"Aborting the OTA update returned an unexpected code: {}",
|
||||||
|
abort_result_code
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CompletedOtaUpdate {
|
||||||
|
partition: *const esp_partition_t,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletedOtaUpdate {
|
||||||
|
/// Sets the boot partition to the newly flashed OTA partition.
|
||||||
|
pub fn set_as_boot_partition(&mut self) -> Result<()> {
|
||||||
|
match unsafe { esp_ota_set_boot_partition(self.partition) } {
|
||||||
|
ESP_OK => Ok(()),
|
||||||
|
ESP_ERR_INVALID_ARG => panic!("Invalid partition sent to esp_ota_set_boot_partition"),
|
||||||
|
ESP_ERR_OTA_VALIDATE_FAILED => Err(Error::from_kind(ErrorKind::InvalidImage)),
|
||||||
|
ESP_ERR_NOT_FOUND => panic!("OTA data partition not found"),
|
||||||
|
ESP_ERR_FLASH_OP_TIMEOUT => Err(Error::from_kind(ErrorKind::FlashTimeout)),
|
||||||
|
ESP_ERR_FLASH_OP_FAIL => Err(Error::from_kind(ErrorKind::FlashFailed)),
|
||||||
|
code => panic!("Unexpected esp_ota_set_boot_partition code: {}", code),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restarts the CPU. If [`set_as_boot_partition`](CompletedOtaUpdate::set_as_boot_partition) was
|
||||||
|
/// called and completed successfully, the CPU will boot into the newly written app.
|
||||||
|
///
|
||||||
|
/// After successful restart, CPU reset reason will be SW_CPU_RESET. Peripherals
|
||||||
|
/// (except for WiFi, BT, UART0, SPI1, and legacy timers) are not reset.
|
||||||
|
pub fn restart(self) -> ! {
|
||||||
|
unsafe { esp_restart() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a raw pointer to the partition that the new app was written to.
|
||||||
|
pub fn raw_partition(&self) -> *const esp_partition_t {
|
||||||
|
self.partition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call this function to indicate that the running app is working well.
|
||||||
|
///
|
||||||
|
/// Should be called (at least) the first time a new app starts up after
|
||||||
|
/// being flashed.
|
||||||
|
pub fn mark_app_valid() {
|
||||||
|
match unsafe { esp_ota_mark_app_valid_cancel_rollback() } {
|
||||||
|
ESP_OK => (),
|
||||||
|
code => panic!(
|
||||||
|
"Unexpected esp_ota_mark_app_valid_cancel_rollback code: {}",
|
||||||
|
code
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call this function to roll back to the previously workable app with reboot.
|
||||||
|
///
|
||||||
|
/// If rolling back failed, it returns an error, otherwise this function never returns,
|
||||||
|
/// as the CPU is rebooting.
|
||||||
|
pub fn rollback_and_reboot() -> Result<core::convert::Infallible> {
|
||||||
|
match unsafe { esp_ota_mark_app_invalid_rollback_and_reboot() } {
|
||||||
|
ESP_FAIL => Err(Error::from_kind(ErrorKind::RollbackFailed)),
|
||||||
|
ESP_ERR_OTA_ROLLBACK_FAILED => Err(Error::from_kind(ErrorKind::RollbackFailedNoApps)),
|
||||||
|
code => panic!(
|
||||||
|
"Unexpected esp_ota_mark_app_invalid_rollback_and_reboot code: {}",
|
||||||
|
code
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
847
rust/src/main.rs
847
rust/src/main.rs
@ -3,12 +3,14 @@ use std::{
|
|||||||
sync::{atomic::AtomicBool, Arc, Mutex},
|
sync::{atomic::AtomicBool, Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use chrono::{DateTime, Datelike, Duration, NaiveDateTime, Timelike};
|
||||||
use chrono::{Datelike, Duration, NaiveDateTime, Timelike};
|
use chrono_tz::{Europe::Berlin, Tz};
|
||||||
use chrono_tz::Europe::Berlin;
|
use config::Plant;
|
||||||
use esp_idf_hal::delay::Delay;
|
use esp_idf_hal::delay::Delay;
|
||||||
use esp_idf_sys::{esp_restart, uxTaskGetStackHighWaterMark, vTaskDelay};
|
use esp_idf_sys::{
|
||||||
use esp_ota::rollback_and_reboot;
|
esp_deep_sleep, esp_restart, gpio_deep_sleep_hold_dis, gpio_deep_sleep_hold_en, vTaskDelay,
|
||||||
|
CONFIG_FREERTOS_HZ,
|
||||||
|
};
|
||||||
use log::error;
|
use log::error;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use plant_hal::{CreatePlantHal, PlantCtrlBoard, PlantCtrlBoardInteraction, PlantHal, PLANT_COUNT};
|
use plant_hal::{CreatePlantHal, PlantCtrlBoard, PlantCtrlBoardInteraction, PlantHal, PLANT_COUNT};
|
||||||
@ -16,9 +18,11 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, WifiConfig},
|
config::{Config, WifiConfig},
|
||||||
|
espota::rollback_and_reboot,
|
||||||
webserver::webserver::{httpd, httpd_initial},
|
webserver::webserver::{httpd, httpd_initial},
|
||||||
};
|
};
|
||||||
mod config;
|
mod config;
|
||||||
|
pub mod espota;
|
||||||
pub mod plant_hal;
|
pub mod plant_hal;
|
||||||
|
|
||||||
const MOIST_SENSOR_MAX_FREQUENCY: u32 = 5200; // 60kHz (500Hz margin)
|
const MOIST_SENSOR_MAX_FREQUENCY: u32 = 5200; // 60kHz (500Hz margin)
|
||||||
@ -30,6 +34,9 @@ const FROM: (f32, f32) = (
|
|||||||
);
|
);
|
||||||
const TO: (f32, f32) = (0_f32, 100_f32);
|
const TO: (f32, f32) = (0_f32, 100_f32);
|
||||||
|
|
||||||
|
pub static BOARD_ACCESS: Lazy<Mutex<PlantCtrlBoard>> = Lazy::new(|| PlantHal::create().unwrap());
|
||||||
|
pub static STAY_ALIVE: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false));
|
||||||
|
|
||||||
mod webserver {
|
mod webserver {
|
||||||
pub mod webserver;
|
pub mod webserver;
|
||||||
}
|
}
|
||||||
@ -39,6 +46,7 @@ enum OnlineMode {
|
|||||||
Offline,
|
Offline,
|
||||||
Wifi,
|
Wifi,
|
||||||
SnTp,
|
SnTp,
|
||||||
|
Online,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
|
||||||
@ -49,65 +57,50 @@ enum WaitType {
|
|||||||
StayAlive,
|
StayAlive,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Default)]
|
||||||
|
struct LightState {
|
||||||
|
active: bool,
|
||||||
|
out_of_work_hour: bool,
|
||||||
|
battery_low: bool,
|
||||||
|
is_day: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Default)]
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Default)]
|
||||||
struct PlantState {
|
struct PlantState {
|
||||||
a: u8,
|
a: Option<u8>,
|
||||||
b: u8,
|
b: Option<u8>,
|
||||||
p: u8,
|
p: Option<u8>,
|
||||||
after_p: u8,
|
after_p: Option<u8>,
|
||||||
|
do_water: bool,
|
||||||
|
frozen: bool,
|
||||||
dry: bool,
|
dry: bool,
|
||||||
active: bool,
|
active: bool,
|
||||||
pump_error: bool,
|
pump_error: bool,
|
||||||
not_effective: bool,
|
not_effective: bool,
|
||||||
cooldown: bool,
|
cooldown: bool,
|
||||||
no_water: bool,
|
no_water: bool,
|
||||||
|
sensor_error_a: Option<SensorError>,
|
||||||
|
sensor_error_b: Option<SensorError>,
|
||||||
|
sensor_error_p: Option<SensorError>,
|
||||||
|
out_of_work_hour: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
|
||||||
let delay = match wait_type {
|
enum SensorError {
|
||||||
WaitType::InitialConfig => 250_u32,
|
Unknown,
|
||||||
WaitType::FlashError => 100_u32,
|
ShortCircuit { hz: f32, max: f32 },
|
||||||
WaitType::NormalConfig => 500_u32,
|
OpenCircuit { hz: f32, min: f32 },
|
||||||
WaitType::StayAlive => 1000_u32,
|
|
||||||
};
|
|
||||||
let led_count = match wait_type {
|
|
||||||
WaitType::InitialConfig => 8,
|
|
||||||
WaitType::FlashError => 8,
|
|
||||||
WaitType::NormalConfig => 4,
|
|
||||||
WaitType::StayAlive => 2,
|
|
||||||
};
|
|
||||||
loop {
|
|
||||||
unsafe {
|
|
||||||
//do not trigger watchdog
|
|
||||||
for i in 0..8 {
|
|
||||||
BOARD_ACCESS.lock().unwrap().fault(i, i < led_count);
|
|
||||||
}
|
|
||||||
BOARD_ACCESS.lock().unwrap().general_fault(true);
|
|
||||||
vTaskDelay(delay);
|
|
||||||
BOARD_ACCESS.lock().unwrap().general_fault(false);
|
|
||||||
for i in 0..8 {
|
|
||||||
BOARD_ACCESS.lock().unwrap().fault(i, false);
|
|
||||||
}
|
|
||||||
vTaskDelay(delay);
|
|
||||||
if wait_type == WaitType::StayAlive
|
|
||||||
&& !STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed)
|
|
||||||
{}
|
|
||||||
if reboot_now.load(std::sync::atomic::Ordering::Relaxed) {
|
|
||||||
println!("Rebooting");
|
|
||||||
esp_restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static BOARD_ACCESS: Lazy<Mutex<PlantCtrlBoard>> = Lazy::new(|| PlantHal::create().unwrap());
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Default)]
|
||||||
pub static STAY_ALIVE: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false));
|
struct TankState {
|
||||||
|
enough_water: bool,
|
||||||
fn map_range(from_range: (f32, f32), to_range: (f32, f32), s: f32) -> f32 {
|
left_ml: u32,
|
||||||
to_range.0 + (s - from_range.0) * (to_range.1 - to_range.0) / (from_range.1 - from_range.0)
|
sensor_error: bool,
|
||||||
|
raw: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn safe_main() -> anyhow::Result<()> {
|
||||||
// It is necessary to call this function once. Otherwise some patches to the runtime
|
// It is necessary to call this function once. Otherwise some patches to the runtime
|
||||||
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
|
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
|
||||||
esp_idf_svc::sys::link_patches();
|
esp_idf_svc::sys::link_patches();
|
||||||
@ -128,9 +121,9 @@ fn main() -> Result<()> {
|
|||||||
let git_hash = env!("VERGEN_GIT_DESCRIBE");
|
let git_hash = env!("VERGEN_GIT_DESCRIBE");
|
||||||
println!("Version useing git has {}", git_hash);
|
println!("Version useing git has {}", git_hash);
|
||||||
|
|
||||||
let mut partition_state: embedded_svc::ota::SlotState = embedded_svc::ota::SlotState::Unknown;
|
let partition_state: embedded_svc::ota::SlotState = embedded_svc::ota::SlotState::Unknown;
|
||||||
// match esp_idf_svc::ota::EspOta::new() {
|
match esp_idf_svc::ota::EspOta::new() {
|
||||||
// Ok(ota) => {
|
Ok(ota) => {
|
||||||
//match ota.get_running_slot(){
|
//match ota.get_running_slot(){
|
||||||
// Ok(slot) => {
|
// Ok(slot) => {
|
||||||
// partition_state = slot.state;
|
// partition_state = slot.state;
|
||||||
@ -143,14 +136,15 @@ fn main() -> Result<()> {
|
|||||||
// println!("Error getting running slot {}", err);
|
// println!("Error getting running slot {}", err);
|
||||||
// },
|
// },
|
||||||
//}
|
//}
|
||||||
// },
|
}
|
||||||
// Err(err) => {
|
Err(err) => {
|
||||||
// println!("Error obtaining ota info {}", err);
|
println!("Error obtaining ota info {}", err);
|
||||||
// },
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
println!("Board hal init");
|
println!("Board hal init");
|
||||||
let mut board: std::sync::MutexGuard<'_, PlantCtrlBoard<'_>> = BOARD_ACCESS.lock().unwrap();
|
let mut board: std::sync::MutexGuard<'_, PlantCtrlBoard<'_>> = BOARD_ACCESS.lock().unwrap();
|
||||||
|
|
||||||
println!("Mounting filesystem");
|
println!("Mounting filesystem");
|
||||||
board.mount_file_system()?;
|
board.mount_file_system()?;
|
||||||
let free_space = board.file_system_size()?;
|
let free_space = board.file_system_size()?;
|
||||||
@ -182,7 +176,7 @@ fn main() -> Result<()> {
|
|||||||
if board.is_config_reset() {
|
if board.is_config_reset() {
|
||||||
board.general_fault(true);
|
board.general_fault(true);
|
||||||
println!("Reset config is pressed, waiting 5s");
|
println!("Reset config is pressed, waiting 5s");
|
||||||
for i in 0..25 {
|
for _i in 0..25 {
|
||||||
board.general_fault(true);
|
board.general_fault(true);
|
||||||
Delay::new_default().delay_ms(50);
|
Delay::new_default().delay_ms(50);
|
||||||
board.general_fault(false);
|
board.general_fault(false);
|
||||||
@ -236,7 +230,7 @@ fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
println!("attempting to connect wifi");
|
println!("attempting to connect wifi");
|
||||||
match board.wifi(&wifi.ssid, wifi.password.as_deref(), 10000) {
|
match board.wifi(wifi.ssid, wifi.password, 5000) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
online_mode = OnlineMode::Wifi;
|
online_mode = OnlineMode::Wifi;
|
||||||
}
|
}
|
||||||
@ -247,7 +241,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if online_mode == OnlineMode::Wifi {
|
if online_mode == OnlineMode::Wifi {
|
||||||
match board.sntp(1000 * 120) {
|
match board.sntp(1000 * 5) {
|
||||||
Ok(new_time) => {
|
Ok(new_time) => {
|
||||||
cur = new_time;
|
cur = new_time;
|
||||||
online_mode = OnlineMode::SnTp;
|
online_mode = OnlineMode::SnTp;
|
||||||
@ -257,10 +251,11 @@ fn main() -> Result<()> {
|
|||||||
board.general_fault(true);
|
board.general_fault(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
println!("Running logic at utc {}", cur);
|
println!("Running logic at utc {}", cur);
|
||||||
let europe_time = cur.with_timezone(&Berlin);
|
let europe_time = cur.with_timezone(&Berlin);
|
||||||
println!("Running logic at europe/berlin {}", europe_time);
|
println!("Running logic at europe/berlin {}", europe_time);
|
||||||
}
|
|
||||||
|
|
||||||
let config: Config;
|
let config: Config;
|
||||||
match board.get_config() {
|
match board.get_config() {
|
||||||
@ -282,6 +277,7 @@ fn main() -> Result<()> {
|
|||||||
match board.mqtt(&config) {
|
match board.mqtt(&config) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("Mqtt connection ready");
|
println!("Mqtt connection ready");
|
||||||
|
online_mode = OnlineMode::Online;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Could not connect mqtt due to {}", err);
|
println!("Could not connect mqtt due to {}", err);
|
||||||
@ -289,29 +285,314 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match board.battery_state() {
|
if online_mode == OnlineMode::Online {
|
||||||
Ok(_state) => {}
|
let _ = board.mqtt_publish(&config, "/firmware/githash", git_hash.as_bytes());
|
||||||
|
let _ = board.mqtt_publish(&config, "/state", "online".as_bytes());
|
||||||
|
|
||||||
|
publish_battery_state(&mut board, &config);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tank_state = determine_tank_state(&mut board, &config);
|
||||||
|
|
||||||
|
let mut water_frozen = false;
|
||||||
|
for _attempt in 0..5 {
|
||||||
|
let water_temperature = board.water_temperature_c();
|
||||||
|
match water_temperature {
|
||||||
|
Ok(temp) => {
|
||||||
|
if online_mode == OnlineMode::Online {
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
"/water/temperature",
|
||||||
|
temp.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
//FIXME mqtt here
|
||||||
|
println!("Water temp is {}", temp);
|
||||||
|
if temp < 4_f32 {
|
||||||
|
water_frozen = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
board.general_fault(true);
|
if online_mode == OnlineMode::Online {
|
||||||
println!("Could not read battery state, assuming low power {}", err);
|
let _ = board.mqtt_publish(&config, "/water/temperature", "Error".as_bytes());
|
||||||
|
}
|
||||||
|
println!("Could not get water temp {}", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut enough_water = true;
|
let mut plantstate = [PlantState {
|
||||||
if config.tank_sensor_enabled {
|
..Default::default()
|
||||||
let tank_value = board.tank_sensor_mv();
|
}; PLANT_COUNT];
|
||||||
match tank_value {
|
let plant_to_pump = determine_next_plant(
|
||||||
Ok(tank_raw) => {
|
online_mode,
|
||||||
//FIXME clear
|
&mut plantstate,
|
||||||
let percent = map_range(
|
europe_time,
|
||||||
(config.tank_empty_mv, config.tank_full_mv),
|
&tank_state,
|
||||||
(0_f32, 100_f32),
|
water_frozen,
|
||||||
tank_raw.into(),
|
&config,
|
||||||
|
&mut board,
|
||||||
);
|
);
|
||||||
let left_ml = ((percent / 100_f32) * config.tank_useable_ml as f32) as u32;
|
|
||||||
|
if STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
drop(board);
|
||||||
|
let reboot_now = Arc::new(AtomicBool::new(false));
|
||||||
|
let _webserver = httpd(reboot_now.clone());
|
||||||
|
wait_infinity(WaitType::StayAlive, reboot_now.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
match plant_to_pump {
|
||||||
|
Some(plant) => {
|
||||||
|
let mut state = plantstate[plant];
|
||||||
|
let consecutive_pump_count = board.consecutive_pump_count(plant) + 1;
|
||||||
|
board.store_consecutive_pump_count(plant, consecutive_pump_count);
|
||||||
|
let plant_config = config.plants[plant];
|
||||||
|
println!(
|
||||||
|
"Trying to pump for {}s with pump {} now",
|
||||||
|
plant_config.pump_time_s, plant
|
||||||
|
);
|
||||||
|
|
||||||
|
board.any_pump(true)?;
|
||||||
|
board.store_last_pump_time(plant, cur);
|
||||||
|
board.pump(plant, true)?;
|
||||||
|
board.last_pump_time(plant);
|
||||||
|
state.active = true;
|
||||||
|
//FIXME do periodic pump test here and state update
|
||||||
|
unsafe { vTaskDelay(plant_config.pump_time_s as u32 * CONFIG_FREERTOS_HZ) };
|
||||||
|
board.pump(plant, false)?;
|
||||||
|
match map_range_moisture(
|
||||||
|
board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32
|
||||||
|
) {
|
||||||
|
Ok(p) => state.after_p = Some(p),
|
||||||
|
Err(err) => {
|
||||||
|
board.fault(plant, true);
|
||||||
|
state.sensor_error_p = Some(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if state.after_p.is_none()
|
||||||
|
|| state.p.is_none()
|
||||||
|
|| state.after_p.unwrap() < state.p.unwrap() + 5
|
||||||
|
{
|
||||||
|
state.pump_error = true;
|
||||||
|
board.fault(plant, true);
|
||||||
|
//mqtt sync pump error value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("Nothing to do");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut light_state = LightState {
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
light_state.is_day = board.is_day();
|
||||||
|
light_state.out_of_work_hour = !in_time_range(
|
||||||
|
europe_time,
|
||||||
|
config.night_lamp_hour_start,
|
||||||
|
config.night_lamp_hour_end,
|
||||||
|
);
|
||||||
|
|
||||||
|
let state_of_charge = board.state_charge_percent().unwrap_or(0);
|
||||||
|
if state_of_charge < 30 {
|
||||||
|
board.set_low_voltage_in_cycle();
|
||||||
|
} else if state_of_charge > 50 {
|
||||||
|
board.clear_low_voltage_in_cycle();
|
||||||
|
}
|
||||||
|
light_state.battery_low = board.low_voltage_in_cycle();
|
||||||
|
|
||||||
|
if !light_state.out_of_work_hour {
|
||||||
|
if config.night_lamp_only_when_dark {
|
||||||
|
if !light_state.is_day {
|
||||||
|
if light_state.battery_low {
|
||||||
|
board.light(false).unwrap();
|
||||||
|
} else {
|
||||||
|
light_state.active = true;
|
||||||
|
board.light(true).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if light_state.battery_low {
|
||||||
|
board.light(false).unwrap();
|
||||||
|
} else {
|
||||||
|
light_state.active = true;
|
||||||
|
board.light(true).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
light_state.active = false;
|
||||||
|
board.light(false).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Lightstate is {:?}", light_state);
|
||||||
|
|
||||||
|
if online_mode == OnlineMode::Online {
|
||||||
|
match serde_json::to_string(&light_state) {
|
||||||
|
Ok(state) => {
|
||||||
|
let _ = board.mqtt_publish(&config, "/light/active", state.as_bytes());
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Error publishing lightstate {}", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//relatch
|
||||||
|
unsafe { gpio_deep_sleep_hold_dis() };
|
||||||
|
unsafe { gpio_deep_sleep_hold_en() };
|
||||||
|
|
||||||
|
//determine next event
|
||||||
|
//is light out of work trigger soon?
|
||||||
|
//is battery low ??
|
||||||
|
//is deep sleep
|
||||||
|
|
||||||
|
unsafe { esp_deep_sleep(1000 * 1000 * 20) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish_battery_state(
|
||||||
|
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||||
|
config: &Config,
|
||||||
|
) {
|
||||||
|
match board.voltage_milli_volt() {
|
||||||
|
Ok(v) => {
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
"/battery/voltage_milli_volt",
|
||||||
|
v.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = board.mqtt_publish(&config, "/battery/voltage_milli_volt", "-1".as_bytes());
|
||||||
|
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match board.average_current_milli_ampere() {
|
||||||
|
Ok(v) => {
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
"/battery/average_current_milli_ampere",
|
||||||
|
v.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
"/battery/average_current_milli_ampere",
|
||||||
|
"-1".as_bytes(),
|
||||||
|
);
|
||||||
|
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match board.cycle_count() {
|
||||||
|
Ok(v) => {
|
||||||
|
let _ = board.mqtt_publish(&config, "/battery/cycle_count", v.to_string().as_bytes());
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = board.mqtt_publish(&config, "/battery/cycle_count", "-1".as_bytes());
|
||||||
|
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match board.design_milli_ampere_hour() {
|
||||||
|
Ok(v) => {
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
"/battery/design_milli_ampere_hour",
|
||||||
|
v.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
"/battery/design_milli_ampere_hour",
|
||||||
|
"-1".as_bytes(),
|
||||||
|
);
|
||||||
|
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match board.max_milli_ampere_hour() {
|
||||||
|
Ok(v) => {
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
"/battery/max_milli_ampere_hour",
|
||||||
|
v.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = board.mqtt_publish(&config, "/battery/max_milli_ampere_hour", "-1".as_bytes());
|
||||||
|
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match board.remaining_milli_ampere_hour() {
|
||||||
|
Ok(v) => {
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
"/battery/remaining_milli_ampere_hour",
|
||||||
|
v.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
"/battery/remaining_milli_ampere_hour",
|
||||||
|
"-1".as_bytes(),
|
||||||
|
);
|
||||||
|
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match board.state_charge_percent() {
|
||||||
|
Ok(v) => {
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
"/battery/state_charge_percent",
|
||||||
|
v.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = board.mqtt_publish(&config, "/battery/state_charge_percent", "-1".as_bytes());
|
||||||
|
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match board.state_health_percent() {
|
||||||
|
Ok(v) => {
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
"/battery/state_health_percent",
|
||||||
|
v.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = board.mqtt_publish(&config, "/battery/state_health_percent", "-1".as_bytes());
|
||||||
|
let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn determine_tank_state(
|
||||||
|
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||||
|
config: &Config,
|
||||||
|
) -> TankState {
|
||||||
|
if config.tank_sensor_enabled {
|
||||||
|
let mut rv: TankState = TankState {
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let success = board
|
||||||
|
.tank_sensor_percent()
|
||||||
|
.and_then(|raw| {
|
||||||
|
rv.raw = raw;
|
||||||
|
return map_range(
|
||||||
|
(
|
||||||
|
config.tank_empty_percent as f32,
|
||||||
|
config.tank_full_percent as f32,
|
||||||
|
),
|
||||||
|
raw as f32,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.and_then(|percent| {
|
||||||
|
rv.left_ml = (percent * config.tank_useable_ml as f32) as u32;
|
||||||
println!(
|
println!(
|
||||||
"Tank sensor returned mv {} as {}% leaving {} ml useable",
|
"Tank sensor returned mv {} as {}% leaving {} ml useable",
|
||||||
tank_raw, percent as u8, left_ml
|
rv.raw, percent as u8, rv.left_ml
|
||||||
);
|
);
|
||||||
if config.tank_warn_percent > percent as u8 {
|
if config.tank_warn_percent > percent as u8 {
|
||||||
board.general_fault(true);
|
board.general_fault(true);
|
||||||
@ -319,127 +600,355 @@ fn main() -> Result<()> {
|
|||||||
"Low water, current percent is {}, minimum warn level is {}",
|
"Low water, current percent is {}, minimum warn level is {}",
|
||||||
percent as u8, config.tank_warn_percent
|
percent as u8, config.tank_warn_percent
|
||||||
);
|
);
|
||||||
//FIXME warn here
|
|
||||||
}
|
}
|
||||||
if config.tank_warn_percent <= 0 {
|
if config.tank_empty_percent > percent as u8 {
|
||||||
enough_water = false;
|
println!(
|
||||||
|
"Empty water, current percent is {}, minimum empty level is {}",
|
||||||
|
percent as u8, config.tank_empty_percent
|
||||||
|
);
|
||||||
|
rv.enough_water = false;
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
});
|
||||||
|
match success {
|
||||||
|
Err(err) => {
|
||||||
|
println!("Could not determine tank value due to {}", err);
|
||||||
|
board.general_fault(true);
|
||||||
|
rv.sensor_error = true;
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
return TankState {
|
||||||
|
enough_water: true,
|
||||||
|
left_ml: 1337,
|
||||||
|
sensor_error: false,
|
||||||
|
raw: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_range(from_range: (f32, f32), s: f32) -> anyhow::Result<f32> {
|
||||||
|
if s < from_range.0 {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Value out of range, min {} but current is {}",
|
||||||
|
from_range.0,
|
||||||
|
s
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if s > from_range.1 {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Value out of range, max {} but current is {}",
|
||||||
|
from_range.1,
|
||||||
|
s
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(TO.0 + (s - from_range.0) * (TO.1 - TO.0) / (from_range.1 - from_range.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_range_moisture(s: f32) -> Result<u8, SensorError> {
|
||||||
|
if s < FROM.0 {
|
||||||
|
return Err(SensorError::OpenCircuit { hz: s, min: FROM.0 });
|
||||||
|
}
|
||||||
|
if s > FROM.1 {
|
||||||
|
return Err(SensorError::ShortCircuit { hz: s, max: FROM.1 });
|
||||||
|
}
|
||||||
|
let tmp = TO.0 + (s - FROM.0) * (TO.1 - TO.0) / (FROM.1 - FROM.0);
|
||||||
|
|
||||||
|
return Ok(tmp as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_time_range(cur: DateTime<Tz>, start: u8, end: u8) -> bool {
|
||||||
|
let curhour = cur.hour() as u8;
|
||||||
|
//eg 10-14
|
||||||
|
if start < end {
|
||||||
|
return curhour > start && curhour < end;
|
||||||
|
} else {
|
||||||
|
//eg 20-05
|
||||||
|
return curhour > start || curhour < end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn option_to_string(value: Option<u8>) -> String {
|
||||||
|
match value {
|
||||||
|
Some(v) => v.to_string(),
|
||||||
|
None => "Error".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn determine_state_target_moisture_for_plant(
|
||||||
|
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||||
|
plant: usize,
|
||||||
|
state: &mut PlantState,
|
||||||
|
config: &Config,
|
||||||
|
tank_state: &TankState,
|
||||||
|
water_frozen: bool,
|
||||||
|
cur: DateTime<Tz>,
|
||||||
|
) {
|
||||||
|
let plant_config = &config.plants[plant];
|
||||||
|
match board.measure_moisture_hz(plant, plant_hal::Sensor::A) {
|
||||||
|
Ok(a) => {
|
||||||
|
let mapped = map_range_moisture(a as f32);
|
||||||
|
match mapped {
|
||||||
|
Ok(result) => state.a = Some(result),
|
||||||
|
Err(err) => {
|
||||||
|
state.sensor_error_a = Some(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
board.general_fault(true);
|
state.sensor_error_a = Some(SensorError::Unknown);
|
||||||
if !config.tank_allow_pumping_if_sensor_error {
|
|
||||||
enough_water = false;
|
|
||||||
}
|
}
|
||||||
//set tank sensor state to fault
|
}
|
||||||
|
match board.measure_moisture_hz(plant, plant_hal::Sensor::B) {
|
||||||
|
Ok(b) => {
|
||||||
|
let mapped = map_range_moisture(b as f32);
|
||||||
|
match mapped {
|
||||||
|
Ok(result) => state.b = Some(result),
|
||||||
|
Err(err) => {
|
||||||
|
state.sensor_error_b = Some(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(_) => {
|
||||||
let plantstate = [PlantState {
|
state.sensor_error_b = Some(SensorError::Unknown);
|
||||||
..Default::default()
|
}
|
||||||
}; PLANT_COUNT];
|
}
|
||||||
for plant in 0..PLANT_COUNT {
|
|
||||||
let mut state = plantstate[plant];
|
|
||||||
//return mapf(mMoisture_raw.getMedian(), MOIST_SENSOR_MIN_FRQ, MOIST_SENSOR_MAX_FRQ, 0, 100);
|
|
||||||
state.a = map_range(
|
|
||||||
FROM,
|
|
||||||
TO,
|
|
||||||
board.measure_moisture_hz(plant, plant_hal::Sensor::A)? as f32,
|
|
||||||
) as u8;
|
|
||||||
state.b = map_range(
|
|
||||||
FROM,
|
|
||||||
TO,
|
|
||||||
board.measure_moisture_hz(plant, plant_hal::Sensor::B)? as f32,
|
|
||||||
) as u8;
|
|
||||||
state.p = map_range(
|
|
||||||
FROM,
|
|
||||||
TO,
|
|
||||||
board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32,
|
|
||||||
) as u8;
|
|
||||||
let plant_config = config.plants[plant];
|
|
||||||
|
|
||||||
//FIXME how to average analyze whatever?
|
//FIXME how to average analyze whatever?
|
||||||
if state.a < plant_config.target_moisture || state.b < plant_config.target_moisture {
|
let a_low = state.a.is_some() && state.a.unwrap() < plant_config.target_moisture;
|
||||||
|
let b_low = state.b.is_some() && state.b.unwrap() < plant_config.target_moisture;
|
||||||
|
|
||||||
|
if a_low || b_low {
|
||||||
state.dry = true;
|
state.dry = true;
|
||||||
if !enough_water {
|
if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error
|
||||||
|
|| !tank_state.enough_water
|
||||||
|
{
|
||||||
state.no_water = true;
|
state.no_water = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let duration = Duration::minutes((plant_config.pump_cooldown_min).into());
|
||||||
let duration = Duration::minutes((60 * plant_config.pump_cooldown_min).into());
|
let next_pump = board.last_pump_time(plant) + duration;
|
||||||
if (board.last_pump_time(plant)? + duration) > cur {
|
if next_pump > cur {
|
||||||
state.cooldown = true;
|
state.cooldown = true;
|
||||||
}
|
}
|
||||||
|
if !in_time_range(
|
||||||
|
cur,
|
||||||
|
plant_config.pump_hour_start,
|
||||||
|
plant_config.pump_hour_end,
|
||||||
|
) {
|
||||||
|
state.out_of_work_hour = true;
|
||||||
|
}
|
||||||
|
if water_frozen {
|
||||||
|
state.frozen = true;
|
||||||
|
}
|
||||||
|
if state.dry && !state.no_water && !state.cooldown && !state.out_of_work_hour {
|
||||||
|
if water_frozen {
|
||||||
|
state.frozen = true;
|
||||||
|
} else {
|
||||||
|
state.do_water = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if state.dry {
|
fn determine_next_plant(
|
||||||
let consecutive_pump_count = board.consecutive_pump_count(plant) + 1;
|
online_mode: OnlineMode,
|
||||||
board.store_consecutive_pump_count(plant, consecutive_pump_count);
|
plantstate: &mut [PlantState; PLANT_COUNT],
|
||||||
if consecutive_pump_count > config.max_consecutive_pump_count.into() {
|
cur: DateTime<Tz>,
|
||||||
|
tank_state: &TankState,
|
||||||
|
water_frozen: bool,
|
||||||
|
config: &Config,
|
||||||
|
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
|
||||||
|
) -> Option<usize> {
|
||||||
|
for plant in 0..PLANT_COUNT {
|
||||||
|
let state = &mut plantstate[plant];
|
||||||
|
let plant_config = &config.plants[plant];
|
||||||
|
match plant_config.mode {
|
||||||
|
config::Mode::OFF => {}
|
||||||
|
config::Mode::TargetMoisture => {
|
||||||
|
determine_state_target_moisture_for_plant(
|
||||||
|
board,
|
||||||
|
plant,
|
||||||
|
state,
|
||||||
|
config,
|
||||||
|
tank_state,
|
||||||
|
water_frozen,
|
||||||
|
cur,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
config::Mode::TimerOnly => {
|
||||||
|
let duration = Duration::minutes((plant_config.pump_cooldown_min).into());
|
||||||
|
let next_pump = board.last_pump_time(plant) + duration;
|
||||||
|
if next_pump > cur {
|
||||||
|
state.cooldown = true;
|
||||||
|
} else {
|
||||||
|
if water_frozen {
|
||||||
|
state.frozen = true;
|
||||||
|
} else {
|
||||||
|
state.do_water = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config::Mode::TimerAndDeadzone => {
|
||||||
|
let duration = Duration::minutes((60 * plant_config.pump_cooldown_min).into());
|
||||||
|
let next_pump = board.last_pump_time(plant) + duration;
|
||||||
|
if next_pump > cur {
|
||||||
|
state.cooldown = true;
|
||||||
|
}
|
||||||
|
if !in_time_range(
|
||||||
|
cur,
|
||||||
|
plant_config.pump_hour_start,
|
||||||
|
plant_config.pump_hour_end,
|
||||||
|
) {
|
||||||
|
state.out_of_work_hour = true;
|
||||||
|
}
|
||||||
|
if !state.cooldown && !state.out_of_work_hour {
|
||||||
|
if water_frozen {
|
||||||
|
state.frozen = true;
|
||||||
|
} else {
|
||||||
|
state.do_water = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.sensor_error_a.is_some()
|
||||||
|
|| state.sensor_error_b.is_some()
|
||||||
|
|| state.sensor_error_p.is_some()
|
||||||
|
{
|
||||||
|
board.fault(plant, true);
|
||||||
|
}
|
||||||
|
if state.do_water {
|
||||||
|
if board.consecutive_pump_count(plant) > config.max_consecutive_pump_count.into() {
|
||||||
state.not_effective = true;
|
state.not_effective = true;
|
||||||
board.fault(plant, true);
|
board.fault(plant, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
board.store_consecutive_pump_count(plant, 0);
|
board.store_consecutive_pump_count(plant, 0);
|
||||||
}
|
}
|
||||||
|
println!("Plant {} state is {:?}", plant, state);
|
||||||
//TODO update mqtt state here!
|
|
||||||
}
|
}
|
||||||
|
if online_mode == OnlineMode::Online {
|
||||||
if (STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed)) {
|
for plant in 0..PLANT_COUNT {
|
||||||
drop(board);
|
let state = &plantstate[plant];
|
||||||
let reboot_now = Arc::new(AtomicBool::new(false));
|
let _ = board.mqtt_publish(
|
||||||
let _webserver = httpd(reboot_now.clone());
|
&config,
|
||||||
wait_infinity(WaitType::StayAlive, reboot_now.clone());
|
format!("/plant{}/Sensor A", plant).as_str(),
|
||||||
}
|
option_to_string(state.a).as_bytes(),
|
||||||
|
);
|
||||||
'eachplant: for plant in 0..PLANT_COUNT {
|
let _ = board.mqtt_publish(
|
||||||
let mut state = plantstate[plant];
|
&config,
|
||||||
if (state.dry && !state.cooldown) {
|
format!("/plant{}/Sensor B", plant).as_str(),
|
||||||
println!("Trying to pump with pump {} now", plant);
|
option_to_string(state.b).as_bytes(),
|
||||||
let plant_config = config.plants[plant];
|
);
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
board.any_pump(true)?;
|
&config,
|
||||||
board.store_last_pump_time(plant, cur);
|
format!("/plant{}/Sensor P before", plant).as_str(),
|
||||||
board.pump(plant, true)?;
|
option_to_string(state.p).as_bytes(),
|
||||||
board.last_pump_time(plant)?;
|
);
|
||||||
state.active = true;
|
let _ = board.mqtt_publish(
|
||||||
unsafe { vTaskDelay(plant_config.pump_time_s.into()) };
|
&config,
|
||||||
state.after_p = map_range(
|
format!("/plant{}/Sensor P after", plant).as_str(),
|
||||||
FROM,
|
option_to_string(state.after_p).as_bytes(),
|
||||||
TO,
|
);
|
||||||
board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32,
|
let _ = board.mqtt_publish(
|
||||||
) as u8;
|
&config,
|
||||||
if state.after_p < state.p + 5 {
|
format!("/plant{}/Should water", plant).as_str(),
|
||||||
state.pump_error = true;
|
state.do_water.to_string().as_bytes(),
|
||||||
board.fault(plant, true);
|
);
|
||||||
}
|
let _ = board.mqtt_publish(
|
||||||
break 'eachplant;
|
&config,
|
||||||
|
format!("/plant{}/Is frozen", plant).as_str(),
|
||||||
|
state.frozen.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
format!("/plant{}/Is dry", plant).as_str(),
|
||||||
|
state.dry.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
format!("/plant{}/Pump Error", plant).as_str(),
|
||||||
|
state.pump_error.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
format!("/plant{}/Pump Ineffective", plant).as_str(),
|
||||||
|
state.not_effective.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
format!("/plant{}/Is in Cooldown", plant).as_str(),
|
||||||
|
state.cooldown.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
format!("/plant{}/No Water", plant).as_str(),
|
||||||
|
state.no_water.to_string().as_bytes(),
|
||||||
|
);
|
||||||
|
let _ = board.mqtt_publish(
|
||||||
|
&config,
|
||||||
|
format!("/plant{}/Out of Work Hour", plant).as_str(),
|
||||||
|
state.out_of_work_hour.to_string().as_bytes(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for plant in 0..PLANT_COUNT {
|
||||||
|
let state = &plantstate[plant];
|
||||||
|
println!(
|
||||||
|
"Checking for water plant {} with state {}",
|
||||||
|
plant, state.do_water
|
||||||
|
);
|
||||||
|
if state.do_water {
|
||||||
|
return Some(plant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("No plant needs water");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
|
||||||
|
let delay = match wait_type {
|
||||||
//check if during light time
|
WaitType::InitialConfig => 250_u32,
|
||||||
//lightstate += out of worktime
|
WaitType::FlashError => 100_u32,
|
||||||
//check battery level
|
WaitType::NormalConfig => 500_u32,
|
||||||
//lightstate += battery empty
|
WaitType::StayAlive => 1000_u32,
|
||||||
//check solar level if config requires
|
};
|
||||||
//lightstate += stillday
|
let led_count = match wait_type {
|
||||||
//if no preventing lightstate, enable light
|
WaitType::InitialConfig => 8,
|
||||||
//lightstate = active
|
WaitType::FlashError => 8,
|
||||||
|
WaitType::NormalConfig => 4,
|
||||||
//keep webserver in scope
|
WaitType::StayAlive => 2,
|
||||||
let webserver = httpd(true);
|
};
|
||||||
let delay = Delay::new_default();
|
|
||||||
loop {
|
loop {
|
||||||
//let freertos do shit
|
unsafe {
|
||||||
delay.delay_ms(1001);
|
//do not trigger watchdog
|
||||||
|
for i in 0..8 {
|
||||||
|
BOARD_ACCESS.lock().unwrap().fault(i, i < led_count);
|
||||||
|
}
|
||||||
|
BOARD_ACCESS.lock().unwrap().general_fault(true);
|
||||||
|
vTaskDelay(delay);
|
||||||
|
BOARD_ACCESS.lock().unwrap().general_fault(false);
|
||||||
|
for i in 0..8 {
|
||||||
|
BOARD_ACCESS.lock().unwrap().fault(i, false);
|
||||||
|
}
|
||||||
|
vTaskDelay(delay);
|
||||||
|
if wait_type == WaitType::StayAlive
|
||||||
|
&& !STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
|
{
|
||||||
|
reboot_now.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if reboot_now.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
println!("Rebooting");
|
||||||
|
esp_restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
//deepsleep here?
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let result = safe_main();
|
||||||
|
result.unwrap();
|
||||||
|
}
|
||||||
//error codes
|
//error codes
|
||||||
//error_reading_config_after_upgrade
|
//error_reading_config_after_upgrade
|
||||||
//error_no_config_after_upgrade
|
//error_no_config_after_upgrade
|
||||||
|
@ -1,35 +1,39 @@
|
|||||||
|
use bq34z100::{Bq34Z100Error, Bq34z100g1, Bq34z100g1Driver};
|
||||||
//mod config;
|
//mod config;
|
||||||
|
use chrono_tz::Europe::Berlin;
|
||||||
use embedded_svc::wifi::{
|
use embedded_svc::wifi::{
|
||||||
AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration,
|
AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration,
|
||||||
};
|
};
|
||||||
|
use esp_idf_hal::i2c::{I2cConfig, I2cDriver, I2cError};
|
||||||
|
use esp_idf_hal::units::FromValueType;
|
||||||
use esp_idf_svc::eventloop::EspSystemEventLoop;
|
use esp_idf_svc::eventloop::EspSystemEventLoop;
|
||||||
|
use esp_idf_svc::mqtt::client::QoS::AtLeastOnce;
|
||||||
use esp_idf_svc::mqtt::client::QoS::ExactlyOnce;
|
use esp_idf_svc::mqtt::client::QoS::ExactlyOnce;
|
||||||
use esp_idf_svc::mqtt::client::{EspMqttClient, MqttClientConfiguration};
|
use esp_idf_svc::mqtt::client::{EspMqttClient, LwtConfiguration, MqttClientConfiguration};
|
||||||
use esp_idf_svc::nvs::EspDefaultNvsPartition;
|
use esp_idf_svc::nvs::EspDefaultNvsPartition;
|
||||||
use esp_idf_svc::wifi::config::{ScanConfig, ScanType};
|
use esp_idf_svc::wifi::config::{ScanConfig, ScanType};
|
||||||
use esp_idf_svc::wifi::EspWifi;
|
use esp_idf_svc::wifi::EspWifi;
|
||||||
|
use measurements::Temperature;
|
||||||
use plant_ctrl2::sipo::ShiftRegister40;
|
use plant_ctrl2::sipo::ShiftRegister40;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use anyhow::{bail, Ok, Result};
|
use anyhow::{bail, Ok, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
|
use ds18b20::Ds18b20;
|
||||||
|
use std::result::Result::Ok as OkStd;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
use embedded_hal::digital::OutputPin;
|
||||||
use ds18b20::Ds18b20;
|
|
||||||
|
|
||||||
use embedded_hal::digital::v2::OutputPin;
|
|
||||||
use esp_idf_hal::adc::{attenuation, AdcChannelDriver, AdcDriver};
|
use esp_idf_hal::adc::{attenuation, AdcChannelDriver, AdcDriver};
|
||||||
use esp_idf_hal::delay::Delay;
|
use esp_idf_hal::delay::Delay;
|
||||||
use esp_idf_hal::gpio::{AnyInputPin, Gpio39, Gpio4, Level, PinDriver};
|
use esp_idf_hal::gpio::{AnyInputPin, Gpio39, Gpio4, InputOutput, Level, PinDriver, Pull};
|
||||||
use esp_idf_hal::pcnt::{
|
use esp_idf_hal::pcnt::{
|
||||||
PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex,
|
PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex,
|
||||||
};
|
};
|
||||||
@ -37,7 +41,7 @@ use esp_idf_hal::prelude::Peripherals;
|
|||||||
use esp_idf_hal::reset::ResetReason;
|
use esp_idf_hal::reset::ResetReason;
|
||||||
use esp_idf_svc::sntp::{self, SyncStatus};
|
use esp_idf_svc::sntp::{self, SyncStatus};
|
||||||
use esp_idf_svc::systime::EspSystemTime;
|
use esp_idf_svc::systime::EspSystemTime;
|
||||||
use esp_idf_sys::{vTaskDelay, EspError};
|
use esp_idf_sys::{esp, gpio_hold_dis, gpio_hold_en, vTaskDelay, EspError};
|
||||||
use one_wire_bus::OneWire;
|
use one_wire_bus::OneWire;
|
||||||
|
|
||||||
use crate::config::{self, Config, WifiConfig};
|
use crate::config::{self, Config, WifiConfig};
|
||||||
@ -48,13 +52,15 @@ const PINS_PER_PLANT: usize = 5;
|
|||||||
const PLANT_PUMP_OFFSET: usize = 0;
|
const PLANT_PUMP_OFFSET: usize = 0;
|
||||||
const PLANT_FAULT_OFFSET: usize = 1;
|
const PLANT_FAULT_OFFSET: usize = 1;
|
||||||
const PLANT_MOIST_PUMP_OFFSET: usize = 2;
|
const PLANT_MOIST_PUMP_OFFSET: usize = 2;
|
||||||
const PLANT_MOIST_B_OFFSET: usize = 3;
|
const PLANT_MOIST_A_OFFSET: usize = 3;
|
||||||
const PLANT_MOIST_A_OFFSET: usize = 4;
|
const PLANT_MOIST_B_OFFSET: usize = 4;
|
||||||
|
|
||||||
const SPIFFS_PARTITION_NAME: &str = "storage";
|
const SPIFFS_PARTITION_NAME: &str = "storage";
|
||||||
const WIFI_CONFIG_FILE: &str = "/spiffs/wifi.cfg";
|
const WIFI_CONFIG_FILE: &str = "/spiffs/wifi.cfg";
|
||||||
const CONFIG_FILE: &str = "/spiffs/config.cfg";
|
const CONFIG_FILE: &str = "/spiffs/config.cfg";
|
||||||
|
|
||||||
|
const TANK_MULTI_SAMPLE: usize = 11;
|
||||||
|
|
||||||
#[link_section = ".rtc.data"]
|
#[link_section = ".rtc.data"]
|
||||||
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
|
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
|
||||||
#[link_section = ".rtc.data"]
|
#[link_section = ".rtc.data"]
|
||||||
@ -62,43 +68,6 @@ static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT];
|
|||||||
#[link_section = ".rtc.data"]
|
#[link_section = ".rtc.data"]
|
||||||
static mut LOW_VOLTAGE_DETECTED: bool = false;
|
static mut LOW_VOLTAGE_DETECTED: bool = false;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct BatteryState {
|
|
||||||
pub state_charge_percent: u8,
|
|
||||||
max_error_percent: u8,
|
|
||||||
remaining_milli_ampere_hour: u32,
|
|
||||||
max_milli_ampere_hour: u32,
|
|
||||||
design_milli_ampere_hour: u32,
|
|
||||||
voltage_milli_volt: u16,
|
|
||||||
average_current_milli_ampere: u16,
|
|
||||||
temperature_tenth_kelvin: u32,
|
|
||||||
average_time_to_empty_minute: u16,
|
|
||||||
average_time_to_full_minute: u16,
|
|
||||||
average_discharge_power_cycle_milli_watt: u16,
|
|
||||||
cycle_count: u16,
|
|
||||||
state_health_percent: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for BatteryState {
|
|
||||||
fn default() -> Self {
|
|
||||||
BatteryState {
|
|
||||||
state_charge_percent: 50,
|
|
||||||
max_error_percent: 100,
|
|
||||||
remaining_milli_ampere_hour: 100,
|
|
||||||
max_milli_ampere_hour: 200,
|
|
||||||
design_milli_ampere_hour: 200,
|
|
||||||
voltage_milli_volt: 12,
|
|
||||||
average_current_milli_ampere: 50,
|
|
||||||
temperature_tenth_kelvin: 1337,
|
|
||||||
average_time_to_empty_minute: 123,
|
|
||||||
average_time_to_full_minute: 123,
|
|
||||||
average_discharge_power_cycle_milli_watt: 123,
|
|
||||||
cycle_count: 123,
|
|
||||||
state_health_percent: 90,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FileSystemSizeInfo {
|
pub struct FileSystemSizeInfo {
|
||||||
pub total_size: usize,
|
pub total_size: usize,
|
||||||
pub used_size: usize,
|
pub used_size: usize,
|
||||||
@ -120,18 +89,30 @@ pub enum Sensor {
|
|||||||
}
|
}
|
||||||
pub trait PlantCtrlBoardInteraction {
|
pub trait PlantCtrlBoardInteraction {
|
||||||
fn time(&mut self) -> Result<chrono::DateTime<Utc>>;
|
fn time(&mut self) -> Result<chrono::DateTime<Utc>>;
|
||||||
fn wifi(&mut self, ssid: &str, password: Option<&str>, max_wait: u32) -> Result<()>;
|
fn wifi(
|
||||||
|
&mut self,
|
||||||
|
ssid: heapless::String<32>,
|
||||||
|
password: Option<heapless::String<64>>,
|
||||||
|
max_wait: u32,
|
||||||
|
) -> Result<()>;
|
||||||
fn sntp(&mut self, max_wait: u32) -> Result<chrono::DateTime<Utc>>;
|
fn sntp(&mut self, max_wait: u32) -> Result<chrono::DateTime<Utc>>;
|
||||||
fn mount_file_system(&mut self) -> Result<()>;
|
fn mount_file_system(&mut self) -> Result<()>;
|
||||||
fn file_system_size(&mut self) -> Result<FileSystemSizeInfo>;
|
fn file_system_size(&mut self) -> Result<FileSystemSizeInfo>;
|
||||||
|
|
||||||
fn battery_state(&mut self) -> Result<BatteryState>;
|
fn state_charge_percent(&mut self) -> Result<u8>;
|
||||||
|
fn remaining_milli_ampere_hour(&mut self) -> Result<u16>;
|
||||||
|
fn max_milli_ampere_hour(&mut self) -> Result<u16>;
|
||||||
|
fn design_milli_ampere_hour(&mut self) -> Result<u16>;
|
||||||
|
fn voltage_milli_volt(&mut self) -> Result<u16>;
|
||||||
|
fn average_current_milli_ampere(&mut self) -> Result<i16>;
|
||||||
|
fn cycle_count(&mut self) -> Result<u16>;
|
||||||
|
fn state_health_percent(&mut self) -> Result<u8>;
|
||||||
|
|
||||||
fn general_fault(&mut self, enable: bool);
|
fn general_fault(&mut self, enable: bool);
|
||||||
|
|
||||||
fn is_day(&self) -> bool;
|
fn is_day(&self) -> bool;
|
||||||
fn water_temperature_c(&mut self) -> Result<f32>;
|
fn water_temperature_c(&mut self) -> Result<f32>;
|
||||||
fn tank_sensor_mv(&mut self) -> Result<u16>;
|
fn tank_sensor_percent(&mut self) -> Result<u16>;
|
||||||
|
|
||||||
fn set_low_voltage_in_cycle(&mut self);
|
fn set_low_voltage_in_cycle(&mut self);
|
||||||
fn clear_low_voltage_in_cycle(&mut self);
|
fn clear_low_voltage_in_cycle(&mut self);
|
||||||
@ -143,7 +124,7 @@ pub trait PlantCtrlBoardInteraction {
|
|||||||
|
|
||||||
fn measure_moisture_hz(&self, plant: usize, sensor: Sensor) -> Result<i32>;
|
fn measure_moisture_hz(&self, plant: usize, sensor: Sensor) -> Result<i32>;
|
||||||
fn pump(&self, plant: usize, enable: bool) -> Result<()>;
|
fn pump(&self, plant: usize, enable: bool) -> Result<()>;
|
||||||
fn last_pump_time(&self, plant: usize) -> Result<chrono::DateTime<Utc>>;
|
fn last_pump_time(&self, plant: usize) -> chrono::DateTime<Utc>;
|
||||||
fn store_last_pump_time(&mut self, plant: usize, time: chrono::DateTime<Utc>);
|
fn store_last_pump_time(&mut self, plant: usize, time: chrono::DateTime<Utc>);
|
||||||
fn store_consecutive_pump_count(&mut self, plant: usize, count: u32);
|
fn store_consecutive_pump_count(&mut self, plant: usize, count: u32);
|
||||||
fn consecutive_pump_count(&mut self, plant: usize) -> u32;
|
fn consecutive_pump_count(&mut self, plant: usize) -> u32;
|
||||||
@ -163,6 +144,7 @@ pub trait PlantCtrlBoardInteraction {
|
|||||||
fn test(&mut self) -> Result<()>;
|
fn test(&mut self) -> Result<()>;
|
||||||
fn is_wifi_config_file_existant(&mut self) -> bool;
|
fn is_wifi_config_file_existant(&mut self) -> bool;
|
||||||
fn mqtt(&mut self, config: &Config) -> Result<()>;
|
fn mqtt(&mut self, config: &Config) -> Result<()>;
|
||||||
|
fn mqtt_publish(&mut self, config: &Config, subtopic: &str, message: &[u8]) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait CreatePlantHal<'a> {
|
pub trait CreatePlantHal<'a> {
|
||||||
@ -173,32 +155,27 @@ pub struct PlantHal {}
|
|||||||
|
|
||||||
pub struct PlantCtrlBoard<'a> {
|
pub struct PlantCtrlBoard<'a> {
|
||||||
shift_register: ShiftRegister40<
|
shift_register: ShiftRegister40<
|
||||||
PinDriver<'a, esp_idf_hal::gpio::Gpio21, esp_idf_hal::gpio::Output>,
|
PinDriver<'a, esp_idf_hal::gpio::Gpio21, InputOutput>,
|
||||||
PinDriver<'a, esp_idf_hal::gpio::Gpio22, esp_idf_hal::gpio::Output>,
|
PinDriver<'a, esp_idf_hal::gpio::Gpio22, InputOutput>,
|
||||||
PinDriver<'a, esp_idf_hal::gpio::Gpio19, esp_idf_hal::gpio::Output>,
|
PinDriver<'a, esp_idf_hal::gpio::Gpio19, InputOutput>,
|
||||||
>,
|
>,
|
||||||
consecutive_watering_plant: Mutex<[u32; PLANT_COUNT]>,
|
|
||||||
last_watering_timestamp: Mutex<[i64; PLANT_COUNT]>,
|
|
||||||
low_voltage_detected: Mutex<bool>,
|
low_voltage_detected: Mutex<bool>,
|
||||||
tank_driver: AdcDriver<'a, esp_idf_hal::adc::ADC1>,
|
tank_driver: AdcDriver<'a, esp_idf_hal::adc::ADC1>,
|
||||||
tank_channel: esp_idf_hal::adc::AdcChannelDriver<'a, { attenuation::DB_11 }, Gpio39>,
|
tank_channel: esp_idf_hal::adc::AdcChannelDriver<'a, { attenuation::DB_11 }, Gpio39>,
|
||||||
solar_is_day: PinDriver<'a, esp_idf_hal::gpio::Gpio25, esp_idf_hal::gpio::Input>,
|
solar_is_day: PinDriver<'a, esp_idf_hal::gpio::Gpio25, esp_idf_hal::gpio::Input>,
|
||||||
boot_button: PinDriver<'a, esp_idf_hal::gpio::Gpio0, esp_idf_hal::gpio::Input>,
|
boot_button: PinDriver<'a, esp_idf_hal::gpio::Gpio0, esp_idf_hal::gpio::Input>,
|
||||||
signal_counter: PcntDriver<'a>,
|
signal_counter: PcntDriver<'a>,
|
||||||
light: PinDriver<'a, esp_idf_hal::gpio::Gpio26, esp_idf_hal::gpio::Output>,
|
light: PinDriver<'a, esp_idf_hal::gpio::Gpio26, InputOutput>,
|
||||||
main_pump: PinDriver<'a, esp_idf_hal::gpio::Gpio23, esp_idf_hal::gpio::Output>,
|
main_pump: PinDriver<'a, esp_idf_hal::gpio::Gpio23, InputOutput>,
|
||||||
tank_power: PinDriver<'a, esp_idf_hal::gpio::Gpio27, esp_idf_hal::gpio::Output>,
|
tank_power: PinDriver<'a, esp_idf_hal::gpio::Gpio27, InputOutput>,
|
||||||
general_fault: PinDriver<'a, esp_idf_hal::gpio::Gpio13, esp_idf_hal::gpio::Output>,
|
general_fault: PinDriver<'a, esp_idf_hal::gpio::Gpio13, InputOutput>,
|
||||||
pub wifi_driver: EspWifi<'a>,
|
pub wifi_driver: EspWifi<'a>,
|
||||||
one_wire_bus: OneWire<PinDriver<'a, Gpio4, esp_idf_hal::gpio::InputOutput>>,
|
one_wire_bus: OneWire<PinDriver<'a, Gpio4, esp_idf_hal::gpio::InputOutput>>,
|
||||||
mqtt_client: Option<EspMqttClient<'a>>,
|
mqtt_client: Option<EspMqttClient<'a>>,
|
||||||
|
battery_driver: Bq34z100g1Driver<I2cDriver<'a>, Delay>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
||||||
fn battery_state(&mut self) -> Result<BatteryState> {
|
|
||||||
return Ok(BatteryState::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_day(&self) -> bool {
|
fn is_day(&self) -> bool {
|
||||||
self.solar_is_day.get_level().into()
|
self.solar_is_day.get_level().into()
|
||||||
}
|
}
|
||||||
@ -230,29 +207,63 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
if sensor_data.temperature == 85_f32 {
|
if sensor_data.temperature == 85_f32 {
|
||||||
bail!("Ds18b20 dummy temperature returned");
|
bail!("Ds18b20 dummy temperature returned");
|
||||||
}
|
}
|
||||||
Ok(sensor_data.temperature)
|
Ok(sensor_data.temperature / 10_f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tank_sensor_mv(&mut self) -> Result<u16> {
|
fn tank_sensor_percent(&mut self) -> Result<u16> {
|
||||||
let delay = Delay::new_default();
|
let delay = Delay::new_default();
|
||||||
self.tank_power.set_high()?;
|
self.tank_power.set_high()?;
|
||||||
//let stabilize
|
//let stabilize
|
||||||
delay.delay_ms(100);
|
delay.delay_ms(100);
|
||||||
|
unsafe {
|
||||||
|
vTaskDelay(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut store = [0_u16; TANK_MULTI_SAMPLE];
|
||||||
|
for multisample in 0..TANK_MULTI_SAMPLE {
|
||||||
let value = self.tank_driver.read(&mut self.tank_channel)?;
|
let value = self.tank_driver.read(&mut self.tank_channel)?;
|
||||||
|
store[multisample] = value;
|
||||||
|
}
|
||||||
|
store.sort();
|
||||||
|
let median = store[6] as f32 / 1000_f32;
|
||||||
|
let config_open_voltage_mv = 3.0;
|
||||||
|
if config_open_voltage_mv < median {
|
||||||
self.tank_power.set_low()?;
|
self.tank_power.set_low()?;
|
||||||
Ok(value)
|
bail!(
|
||||||
|
"Tank sensor missing, open loop voltage {} on tank sensor input {}",
|
||||||
|
config_open_voltage_mv,
|
||||||
|
median
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let r2 = median * 50.0 / (3.3 - median);
|
||||||
|
let mut percent = r2 / 190_f32 * 100_f32;
|
||||||
|
percent = percent.clamp(0.0, 100.0);
|
||||||
|
|
||||||
|
let quantizised = quantize_to_next_5_percent(percent as f64) as u16;
|
||||||
|
println!(
|
||||||
|
"Tank sensor raw {} percent {} quantized {}",
|
||||||
|
median, percent, quantizised
|
||||||
|
);
|
||||||
|
return Ok(quantizised);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_low_voltage_in_cycle(&mut self) {
|
fn set_low_voltage_in_cycle(&mut self) {
|
||||||
*self.low_voltage_detected.get_mut().unwrap() = true;
|
unsafe {
|
||||||
|
LOW_VOLTAGE_DETECTED = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_low_voltage_in_cycle(&mut self) {
|
fn clear_low_voltage_in_cycle(&mut self) {
|
||||||
*self.low_voltage_detected.get_mut().unwrap() = false;
|
unsafe {
|
||||||
|
LOW_VOLTAGE_DETECTED = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn light(&mut self, enable: bool) -> Result<()> {
|
fn light(&mut self, enable: bool) -> Result<()> {
|
||||||
|
unsafe { gpio_hold_dis(self.light.pin()) };
|
||||||
self.light.set_state(enable.into())?;
|
self.light.set_state(enable.into())?;
|
||||||
|
unsafe { gpio_hold_en(self.light.pin()) };
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,23 +276,28 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last_pump_time(&self, plant: usize) -> Result<chrono::DateTime<Utc>> {
|
fn last_pump_time(&self, plant: usize) -> chrono::DateTime<Utc> {
|
||||||
let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant];
|
let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant];
|
||||||
let timestamp = NaiveDateTime::from_timestamp_millis(ts)
|
let timestamp = NaiveDateTime::from_timestamp_millis(ts).unwrap();
|
||||||
.ok_or(anyhow!("could not convert timestamp"))?;
|
DateTime::<Utc>::from_naive_utc_and_offset(timestamp, Utc)
|
||||||
Ok(DateTime::<Utc>::from_naive_utc_and_offset(timestamp, Utc))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store_last_pump_time(&mut self, plant: usize, time: chrono::DateTime<Utc>) {
|
fn store_last_pump_time(&mut self, plant: usize, time: chrono::DateTime<Utc>) {
|
||||||
self.last_watering_timestamp.get_mut().unwrap()[plant] = time.timestamp_millis();
|
unsafe {
|
||||||
|
LAST_WATERING_TIMESTAMP[plant] = time.timestamp_millis();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store_consecutive_pump_count(&mut self, plant: usize, count: u32) {
|
fn store_consecutive_pump_count(&mut self, plant: usize, count: u32) {
|
||||||
self.consecutive_watering_plant.get_mut().unwrap()[plant] = count;
|
unsafe {
|
||||||
|
CONSECUTIVE_WATERING_PLANT[plant] = count;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn consecutive_pump_count(&mut self, plant: usize) -> u32 {
|
fn consecutive_pump_count(&mut self, plant: usize) -> u32 {
|
||||||
return self.consecutive_watering_plant.get_mut().unwrap()[plant];
|
unsafe {
|
||||||
|
return CONSECUTIVE_WATERING_PLANT[plant];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fault(&self, plant: usize, enable: bool) {
|
fn fault(&self, plant: usize, enable: bool) {
|
||||||
@ -292,7 +308,9 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn low_voltage_in_cycle(&mut self) -> bool {
|
fn low_voltage_in_cycle(&mut self) -> bool {
|
||||||
return *self.low_voltage_detected.get_mut().unwrap();
|
unsafe {
|
||||||
|
return LOW_VOLTAGE_DETECTED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn any_pump(&mut self, enable: bool) -> Result<()> {
|
fn any_pump(&mut self, enable: bool) -> Result<()> {
|
||||||
@ -338,7 +356,7 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
|
|
||||||
let delay = Delay::new_default();
|
let delay = Delay::new_default();
|
||||||
let measurement = 100;
|
let measurement = 100;
|
||||||
let factor = 1000 / 100;
|
let factor = 1000 as f32 / measurement as f32;
|
||||||
|
|
||||||
self.shift_register.decompose()[index].set_high().unwrap();
|
self.shift_register.decompose()[index].set_high().unwrap();
|
||||||
//give some time to stabilize
|
//give some time to stabilize
|
||||||
@ -348,7 +366,7 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
self.signal_counter.counter_pause()?;
|
self.signal_counter.counter_pause()?;
|
||||||
self.shift_register.decompose()[index].set_low().unwrap();
|
self.shift_register.decompose()[index].set_low().unwrap();
|
||||||
let unscaled = self.signal_counter.get_counter_value()? as i32;
|
let unscaled = self.signal_counter.get_counter_value()? as i32;
|
||||||
let hz = unscaled * factor;
|
let hz = (unscaled as f32 * factor) as i32;
|
||||||
println!("Measuring {:?} @ {} with {}", sensor, plant, hz);
|
println!("Measuring {:?} @ {} with {}", sensor, plant, hz);
|
||||||
Ok(hz)
|
Ok(hz)
|
||||||
}
|
}
|
||||||
@ -359,7 +377,7 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
|
|
||||||
fn wifi_ap(&mut self) -> Result<()> {
|
fn wifi_ap(&mut self) -> Result<()> {
|
||||||
let apconfig = AccessPointConfiguration {
|
let apconfig = AccessPointConfiguration {
|
||||||
ssid: "PlantCtrl".into(),
|
ssid: heapless::String::from_str("PlantCtrl").unwrap(),
|
||||||
auth_method: AuthMethod::None,
|
auth_method: AuthMethod::None,
|
||||||
ssid_hidden: false,
|
ssid_hidden: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@ -371,14 +389,19 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wifi(&mut self, ssid: &str, password: Option<&str>, max_wait: u32) -> Result<()> {
|
fn wifi(
|
||||||
|
&mut self,
|
||||||
|
ssid: heapless::String<32>,
|
||||||
|
password: Option<heapless::String<64>>,
|
||||||
|
max_wait: u32,
|
||||||
|
) -> Result<()> {
|
||||||
match password {
|
match password {
|
||||||
Some(pw) => {
|
Some(pw) => {
|
||||||
//TODO expect error due to invalid pw or similar! //call this during configuration and check if works, revert to config mode if not
|
//TODO expect error due to invalid pw or similar! //call this during configuration and check if works, revert to config mode if not
|
||||||
self.wifi_driver.set_configuration(&Configuration::Client(
|
self.wifi_driver.set_configuration(&Configuration::Client(
|
||||||
ClientConfiguration {
|
ClientConfiguration {
|
||||||
ssid: ssid.into(),
|
ssid: ssid,
|
||||||
password: pw.into(),
|
password: pw,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
))?;
|
))?;
|
||||||
@ -386,7 +409,7 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
None => {
|
None => {
|
||||||
self.wifi_driver
|
self.wifi_driver
|
||||||
.set_configuration(&Configuration::Client(ClientConfiguration {
|
.set_configuration(&Configuration::Client(ClientConfiguration {
|
||||||
ssid: ssid.into(),
|
ssid: ssid,
|
||||||
auth_method: AuthMethod::None,
|
auth_method: AuthMethod::None,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}))
|
}))
|
||||||
@ -401,7 +424,6 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
let mut counter = 0_u32;
|
let mut counter = 0_u32;
|
||||||
while !self.wifi_driver.is_connected()? {
|
while !self.wifi_driver.is_connected()? {
|
||||||
println!("Waiting for station connection");
|
println!("Waiting for station connection");
|
||||||
//TODO blink status?
|
|
||||||
delay.delay_ms(250);
|
delay.delay_ms(250);
|
||||||
counter += 250;
|
counter += 250;
|
||||||
if counter > max_wait {
|
if counter > max_wait {
|
||||||
@ -501,7 +523,11 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
|
|
||||||
fn get_config(&mut self) -> Result<config::Config> {
|
fn get_config(&mut self) -> Result<config::Config> {
|
||||||
let cfg = File::open(CONFIG_FILE)?;
|
let cfg = File::open(CONFIG_FILE)?;
|
||||||
let config: Config = serde_json::from_reader(cfg)?;
|
let mut config: Config = serde_json::from_reader(cfg)?;
|
||||||
|
//remove duplicate end of topic
|
||||||
|
if config.base_topic.ends_with("/") {
|
||||||
|
config.base_topic.pop();
|
||||||
|
}
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -589,8 +615,15 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn mqtt(&mut self, config: &Config) -> Result<()> {
|
fn mqtt(&mut self, config: &Config) -> Result<()> {
|
||||||
//FIXME testament
|
let last_will_topic = format!("{}/state", config.base_topic);
|
||||||
|
|
||||||
let mqtt_client_config = MqttClientConfiguration {
|
let mqtt_client_config = MqttClientConfiguration {
|
||||||
|
lwt: Some(LwtConfiguration {
|
||||||
|
topic: &last_will_topic,
|
||||||
|
payload: "lost".as_bytes(),
|
||||||
|
qos: AtLeastOnce,
|
||||||
|
retain: true,
|
||||||
|
}),
|
||||||
//room for improvement
|
//room for improvement
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@ -605,14 +638,17 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
let round_trip_topic_copy = round_trip_topic.clone();
|
let round_trip_topic_copy = round_trip_topic.clone();
|
||||||
let round_trip_ok_copy = round_trip_ok.clone();
|
let round_trip_ok_copy = round_trip_ok.clone();
|
||||||
let mut client =
|
let mut client =
|
||||||
EspMqttClient::new(&config.mqtt_url, &mqtt_client_config, move |handler| {
|
EspMqttClient::new_cb(&config.mqtt_url, &mqtt_client_config, move |event| {
|
||||||
match handler {
|
let payload = event.payload();
|
||||||
Err(err) => println!("Ignoring damaged message {}", err),
|
match payload {
|
||||||
core::result::Result::Ok(event) => {
|
embedded_svc::mqtt::client::EventPayload::Received {
|
||||||
match event {
|
id: _,
|
||||||
embedded_svc::mqtt::client::Event::Received(msg) => {
|
topic,
|
||||||
let data = String::from_utf8_lossy(msg.data());
|
data,
|
||||||
if let Some(topic) = msg.topic() {
|
details: _,
|
||||||
|
} => {
|
||||||
|
let data = String::from_utf8_lossy(data);
|
||||||
|
if let Some(topic) = topic {
|
||||||
//todo use enums
|
//todo use enums
|
||||||
if topic.eq(round_trip_topic_copy.as_str()) {
|
if topic.eq(round_trip_topic_copy.as_str()) {
|
||||||
round_trip_ok_copy
|
round_trip_ok_copy
|
||||||
@ -621,8 +657,7 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
let value = data.eq_ignore_ascii_case("true")
|
let value = data.eq_ignore_ascii_case("true")
|
||||||
|| data.eq_ignore_ascii_case("1");
|
|| data.eq_ignore_ascii_case("1");
|
||||||
println!("Received stay alive with value {}", value);
|
println!("Received stay alive with value {}", value);
|
||||||
STAY_ALIVE
|
STAY_ALIVE.store(value, std::sync::atomic::Ordering::Relaxed);
|
||||||
.store(value, std::sync::atomic::Ordering::Relaxed);
|
|
||||||
} else {
|
} else {
|
||||||
println!("Unknown topic recieved {}", topic);
|
println!("Unknown topic recieved {}", topic);
|
||||||
}
|
}
|
||||||
@ -630,8 +665,6 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
})?;
|
})?;
|
||||||
//subscribe to roundtrip
|
//subscribe to roundtrip
|
||||||
|
|
||||||
@ -660,17 +693,191 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> {
|
|||||||
}
|
}
|
||||||
bail!("Mqtt did not complete roundtrip in time");
|
bail!("Mqtt did not complete roundtrip in time");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mqtt_publish(&mut self, config: &Config, subtopic: &str, message: &[u8]) -> Result<()> {
|
||||||
|
if !subtopic.starts_with("/") {
|
||||||
|
println!("Subtopic without / at start {}", subtopic);
|
||||||
|
bail!("Subtopic without / at start {}", subtopic);
|
||||||
|
}
|
||||||
|
if subtopic.len() > 192 {
|
||||||
|
println!("Subtopic exceeds 192 chars {}", subtopic);
|
||||||
|
bail!("Subtopic exceeds 192 chars {}", subtopic);
|
||||||
|
}
|
||||||
|
if self.mqtt_client.is_none() {
|
||||||
|
println!("Not connected to mqtt");
|
||||||
|
bail!("Not connected to mqtt");
|
||||||
|
}
|
||||||
|
let client = self.mqtt_client.as_mut().unwrap();
|
||||||
|
|
||||||
|
let mut full_topic: heapless::String<256> = heapless::String::new();
|
||||||
|
if full_topic.push_str(&config.base_topic).is_err() {
|
||||||
|
println!("Some error assembling full_topic 1");
|
||||||
|
bail!("Some error assembling full_topic 1")
|
||||||
|
};
|
||||||
|
if full_topic.push_str(subtopic).is_err() {
|
||||||
|
println!("Some error assembling full_topic 2");
|
||||||
|
bail!("Some error assembling full_topic 2")
|
||||||
|
};
|
||||||
|
client.publish(
|
||||||
|
&full_topic,
|
||||||
|
embedded_svc::mqtt::client::QoS::ExactlyOnce,
|
||||||
|
true,
|
||||||
|
message,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_charge_percent(&mut self) -> Result<u8> {
|
||||||
|
match self.battery_driver.state_of_charge() {
|
||||||
|
OkStd(r) => Ok(r),
|
||||||
|
Err(err) => bail!("Error reading SoC {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remaining_milli_ampere_hour(&mut self) -> Result<u16> {
|
||||||
|
match self.battery_driver.remaining_capacity() {
|
||||||
|
OkStd(r) => Ok(r),
|
||||||
|
Err(err) => bail!("Error reading Remaining Capacity {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_milli_ampere_hour(&mut self) -> Result<u16> {
|
||||||
|
match self.battery_driver.full_charge_capacity() {
|
||||||
|
OkStd(r) => Ok(r),
|
||||||
|
Err(err) => bail!("Error reading Full Charge Capacity {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn design_milli_ampere_hour(&mut self) -> Result<u16> {
|
||||||
|
match self.battery_driver.design_capacity() {
|
||||||
|
OkStd(r) => Ok(r),
|
||||||
|
Err(err) => bail!("Error reading Design Capacity {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voltage_milli_volt(&mut self) -> Result<u16> {
|
||||||
|
return match self.battery_driver.voltage() {
|
||||||
|
OkStd(r) => Ok(r),
|
||||||
|
Err(err) => bail!("Error reading voltage {:?}", err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn average_current_milli_ampere(&mut self) -> Result<i16> {
|
||||||
|
match self.battery_driver.average_current() {
|
||||||
|
OkStd(r) => Ok(r),
|
||||||
|
Err(err) => bail!("Error reading Average Current {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cycle_count(&mut self) -> Result<u16> {
|
||||||
|
match self.battery_driver.cycle_count() {
|
||||||
|
OkStd(r) => Ok(r),
|
||||||
|
Err(err) => bail!("Error reading Cycle Count {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_health_percent(&mut self) -> Result<u8> {
|
||||||
|
match self.battery_driver.state_of_health() {
|
||||||
|
OkStd(r) => Ok(r as u8),
|
||||||
|
Err(err) => bail!("Error reading State of Health {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_battery(
|
||||||
|
battery_driver: &mut Bq34z100g1Driver<I2cDriver, Delay>,
|
||||||
|
) -> Result<(), Bq34Z100Error<I2cError>> {
|
||||||
|
let fwversion = battery_driver.fw_version().unwrap_or_else(|e| {
|
||||||
|
println!("Firmeware {:?}", e);
|
||||||
|
0
|
||||||
|
});
|
||||||
|
println!("fw version is {}", fwversion);
|
||||||
|
|
||||||
|
let design_capacity = battery_driver.design_capacity().unwrap_or_else(|e| {
|
||||||
|
println!("Design capacity {:?}", e);
|
||||||
|
0
|
||||||
|
});
|
||||||
|
println!("Design Capacity {}", design_capacity);
|
||||||
|
if design_capacity == 1000 {
|
||||||
|
println!("Still stock configuring battery, readouts are likely to be wrong!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let flags = battery_driver.get_flags_decoded()?;
|
||||||
|
println!("Flags {:?}", flags);
|
||||||
|
|
||||||
|
let chem_id = battery_driver.chem_id().unwrap_or_else(|e| {
|
||||||
|
println!("Chemid {:?}", e);
|
||||||
|
0
|
||||||
|
});
|
||||||
|
|
||||||
|
let bat_temp = battery_driver.internal_temperature().unwrap_or_else(|e| {
|
||||||
|
println!("Bat Temp {:?}", e);
|
||||||
|
0
|
||||||
|
});
|
||||||
|
let temp_c = Temperature::from_kelvin(bat_temp as f64 / 10_f64).as_celsius();
|
||||||
|
let voltage = battery_driver.voltage().unwrap_or_else(|e| {
|
||||||
|
println!("Bat volt {:?}", e);
|
||||||
|
0
|
||||||
|
});
|
||||||
|
let current = battery_driver.current().unwrap_or_else(|e| {
|
||||||
|
println!("Bat current {:?}", e);
|
||||||
|
0
|
||||||
|
});
|
||||||
|
let state = battery_driver.state_of_charge().unwrap_or_else(|e| {
|
||||||
|
println!("Bat Soc {:?}", e);
|
||||||
|
0
|
||||||
|
});
|
||||||
|
let charge_voltage = battery_driver.charge_voltage().unwrap_or_else(|e| {
|
||||||
|
println!("Bat Charge Volt {:?}", e);
|
||||||
|
0
|
||||||
|
});
|
||||||
|
let charge_current = battery_driver.charge_current().unwrap_or_else(|e| {
|
||||||
|
println!("Bat Charge Current {:?}", e);
|
||||||
|
0
|
||||||
|
});
|
||||||
|
println!("ChemId: {} Current voltage {} and current {} with charge {}% and temp {} CVolt: {} CCur {}", chem_id, voltage, current, state, temp_c, charge_voltage, charge_current);
|
||||||
|
let _ = battery_driver.unsealed();
|
||||||
|
let _ = battery_driver.it_enable();
|
||||||
|
return Result::Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreatePlantHal<'_> for PlantHal {
|
impl CreatePlantHal<'_> for PlantHal {
|
||||||
fn create() -> Result<Mutex<PlantCtrlBoard<'static>>> {
|
fn create() -> Result<Mutex<PlantCtrlBoard<'static>>> {
|
||||||
let peripherals = Peripherals::take()?;
|
let peripherals = Peripherals::take()?;
|
||||||
|
|
||||||
let clock = PinDriver::output(peripherals.pins.gpio21)?;
|
let i2c = peripherals.i2c1;
|
||||||
let latch = PinDriver::output(peripherals.pins.gpio22)?;
|
let config = I2cConfig::new()
|
||||||
let data = PinDriver::output(peripherals.pins.gpio19)?;
|
.scl_enable_pullup(false)
|
||||||
|
.sda_enable_pullup(false)
|
||||||
|
.baudrate(10_u32.kHz().into());
|
||||||
|
let scl = peripherals.pins.gpio16;
|
||||||
|
let sda = peripherals.pins.gpio17;
|
||||||
|
|
||||||
let one_wire_pin = PinDriver::input_output_od(peripherals.pins.gpio4)?;
|
let driver = I2cDriver::new(i2c, sda, scl, &config).unwrap();
|
||||||
|
|
||||||
|
let i2c_port = driver.port();
|
||||||
|
esp!(unsafe { esp_idf_sys::i2c_set_timeout(i2c_port, 1048000) }).unwrap();
|
||||||
|
|
||||||
|
let mut battery_driver: Bq34z100g1Driver<I2cDriver, Delay> = Bq34z100g1Driver {
|
||||||
|
i2c: driver,
|
||||||
|
delay: Delay::new_default(),
|
||||||
|
flash_block_data: [0; 32],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut clock = PinDriver::input_output(peripherals.pins.gpio21)?;
|
||||||
|
clock.set_pull(Pull::Floating).unwrap();
|
||||||
|
let mut latch = PinDriver::input_output(peripherals.pins.gpio22)?;
|
||||||
|
latch.set_pull(Pull::Floating).unwrap();
|
||||||
|
let mut data = PinDriver::input_output(peripherals.pins.gpio19)?;
|
||||||
|
data.set_pull(Pull::Floating).unwrap();
|
||||||
|
let shift_register = ShiftRegister40::new(clock.into(), latch.into(), data.into());
|
||||||
|
for mut pin in shift_register.decompose() {
|
||||||
|
pin.set_low().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut one_wire_pin = PinDriver::input_output_od(peripherals.pins.gpio4)?;
|
||||||
|
one_wire_pin.set_pull(Pull::Floating).unwrap();
|
||||||
//TODO make to none if not possible to init
|
//TODO make to none if not possible to init
|
||||||
|
|
||||||
//init,reset rtc memory depending on cause
|
//init,reset rtc memory depending on cause
|
||||||
@ -697,6 +904,30 @@ impl CreatePlantHal<'_> for PlantHal {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
println!("Keeping RTC store");
|
println!("Keeping RTC store");
|
||||||
|
unsafe {
|
||||||
|
println!(
|
||||||
|
"Current low voltage detection is {:?}",
|
||||||
|
LOW_VOLTAGE_DETECTED
|
||||||
|
);
|
||||||
|
for i in 0..PLANT_COUNT {
|
||||||
|
let smaller_time = LAST_WATERING_TIMESTAMP[i];
|
||||||
|
let local_time = NaiveDateTime::from_timestamp_millis(smaller_time)
|
||||||
|
.ok_or(anyhow!("could not convert timestamp"))?;
|
||||||
|
let utc_time = local_time.and_utc();
|
||||||
|
let europe_time = utc_time.with_timezone(&Berlin);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"LAST_WATERING_TIMESTAMP[{}] = {} as europe {}",
|
||||||
|
i, LAST_WATERING_TIMESTAMP[i], europe_time
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for i in 0..PLANT_COUNT {
|
||||||
|
println!(
|
||||||
|
"CONSECUTIVE_WATERING_PLANT[{}] = {}",
|
||||||
|
i, CONSECUTIVE_WATERING_PLANT[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut counter_unit1 = PcntDriver::new(
|
let mut counter_unit1 = PcntDriver::new(
|
||||||
@ -714,10 +945,10 @@ impl CreatePlantHal<'_> for PlantHal {
|
|||||||
PinIndex::Pin0,
|
PinIndex::Pin0,
|
||||||
PinIndex::Pin1,
|
PinIndex::Pin1,
|
||||||
&PcntChannelConfig {
|
&PcntChannelConfig {
|
||||||
lctrl_mode: PcntControlMode::Reverse,
|
lctrl_mode: PcntControlMode::Keep,
|
||||||
hctrl_mode: PcntControlMode::Keep,
|
hctrl_mode: PcntControlMode::Keep,
|
||||||
pos_mode: PcntCountMode::Decrement,
|
pos_mode: PcntCountMode::Increment,
|
||||||
neg_mode: PcntCountMode::Increment,
|
neg_mode: PcntCountMode::Hold,
|
||||||
counter_h_lim: i16::MAX,
|
counter_h_lim: i16::MAX,
|
||||||
counter_l_lim: 0,
|
counter_l_lim: 0,
|
||||||
},
|
},
|
||||||
@ -735,29 +966,44 @@ impl CreatePlantHal<'_> for PlantHal {
|
|||||||
let nvs = EspDefaultNvsPartition::take()?;
|
let nvs = EspDefaultNvsPartition::take()?;
|
||||||
let wifi_driver = EspWifi::new(peripherals.modem, sys_loop, Some(nvs))?;
|
let wifi_driver = EspWifi::new(peripherals.modem, sys_loop, Some(nvs))?;
|
||||||
|
|
||||||
let shift_register = ShiftRegister40::new(clock, latch, data);
|
|
||||||
let last_watering_timestamp = Mutex::new(unsafe { LAST_WATERING_TIMESTAMP });
|
|
||||||
let consecutive_watering_plant = Mutex::new(unsafe { CONSECUTIVE_WATERING_PLANT });
|
|
||||||
let low_voltage_detected = Mutex::new(unsafe { LOW_VOLTAGE_DETECTED });
|
let low_voltage_detected = Mutex::new(unsafe { LOW_VOLTAGE_DETECTED });
|
||||||
let tank_driver =
|
|
||||||
AdcDriver::new(peripherals.adc1, &esp_idf_hal::adc::config::Config::new())?;
|
let adc_config = esp_idf_hal::adc::config::Config {
|
||||||
|
resolution: esp_idf_hal::adc::config::Resolution::Resolution12Bit,
|
||||||
|
calibration: true,
|
||||||
|
};
|
||||||
|
let tank_driver = AdcDriver::new(peripherals.adc1, &adc_config)?;
|
||||||
let tank_channel: AdcChannelDriver<'_, { attenuation::DB_11 }, Gpio39> =
|
let tank_channel: AdcChannelDriver<'_, { attenuation::DB_11 }, Gpio39> =
|
||||||
AdcChannelDriver::new(peripherals.pins.gpio39)?;
|
AdcChannelDriver::new(peripherals.pins.gpio39)?;
|
||||||
let solar_is_day = PinDriver::input(peripherals.pins.gpio25)?;
|
|
||||||
let boot_button = PinDriver::input(peripherals.pins.gpio0)?;
|
let mut solar_is_day = PinDriver::input(peripherals.pins.gpio25)?;
|
||||||
let light = PinDriver::output(peripherals.pins.gpio26)?;
|
solar_is_day.set_pull(Pull::Floating)?;
|
||||||
let main_pump = PinDriver::output(peripherals.pins.gpio23)?;
|
|
||||||
let tank_power = PinDriver::output(peripherals.pins.gpio27)?;
|
let mut boot_button = PinDriver::input(peripherals.pins.gpio0)?;
|
||||||
let general_fault = PinDriver::output(peripherals.pins.gpio13)?;
|
boot_button.set_pull(Pull::Floating)?;
|
||||||
|
|
||||||
|
let mut light = PinDriver::input_output(peripherals.pins.gpio26)?;
|
||||||
|
light.set_pull(Pull::Floating).unwrap();
|
||||||
|
|
||||||
|
let mut main_pump = PinDriver::input_output(peripherals.pins.gpio23)?;
|
||||||
|
main_pump.set_pull(Pull::Floating)?;
|
||||||
|
main_pump.set_low()?;
|
||||||
|
let mut tank_power = PinDriver::input_output(peripherals.pins.gpio27)?;
|
||||||
|
tank_power.set_pull(Pull::Floating)?;
|
||||||
|
let mut general_fault = PinDriver::input_output(peripherals.pins.gpio13)?;
|
||||||
|
general_fault.set_pull(Pull::Floating)?;
|
||||||
|
general_fault.set_low()?;
|
||||||
let one_wire_bus = OneWire::new(one_wire_pin)
|
let one_wire_bus = OneWire::new(one_wire_pin)
|
||||||
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
|
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
|
||||||
|
|
||||||
println!("After stuff");
|
println!("After stuff");
|
||||||
|
|
||||||
|
let status = print_battery(&mut battery_driver);
|
||||||
|
if status.is_err() {
|
||||||
|
println!("Error communicating with battery!! {:?}", status.err());
|
||||||
|
}
|
||||||
let rv = Mutex::new(PlantCtrlBoard {
|
let rv = Mutex::new(PlantCtrlBoard {
|
||||||
shift_register,
|
shift_register,
|
||||||
last_watering_timestamp,
|
|
||||||
consecutive_watering_plant,
|
|
||||||
low_voltage_detected,
|
low_voltage_detected,
|
||||||
tank_driver,
|
tank_driver,
|
||||||
tank_channel,
|
tank_channel,
|
||||||
@ -771,7 +1017,26 @@ impl CreatePlantHal<'_> for PlantHal {
|
|||||||
signal_counter: counter_unit1,
|
signal_counter: counter_unit1,
|
||||||
wifi_driver,
|
wifi_driver,
|
||||||
mqtt_client: None,
|
mqtt_client: None,
|
||||||
|
battery_driver,
|
||||||
});
|
});
|
||||||
Ok(rv)
|
Ok(rv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn quantize_to_next_5_percent(value: f64) -> i32 {
|
||||||
|
// Multiply by 100 to work with integer values
|
||||||
|
let multiplied_value = (value * 100.0).round() as i32;
|
||||||
|
|
||||||
|
// Calculate the remainder when divided by 5
|
||||||
|
let remainder = multiplied_value % 5;
|
||||||
|
|
||||||
|
// If the remainder is greater than or equal to half of 5, round up to the next 5%
|
||||||
|
let rounded_value = if remainder >= 2 {
|
||||||
|
multiplied_value + (5 - remainder)
|
||||||
|
} else {
|
||||||
|
multiplied_value - remainder
|
||||||
|
};
|
||||||
|
|
||||||
|
// Divide by 100 to get back to a float
|
||||||
|
rounded_value / 100
|
||||||
|
}
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
use core::cell::RefCell;
|
use core::cell::RefCell;
|
||||||
use core::mem::{self, MaybeUninit};
|
use core::mem::{self, MaybeUninit};
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
use crate::hal::digital::v2::OutputPin;
|
use hal::digital::OutputPin;
|
||||||
|
|
||||||
trait ShiftRegisterInternal {
|
trait ShiftRegisterInternal {
|
||||||
fn update(&self, index: usize, command: bool) -> Result<(), ()>;
|
fn update(&self, index: usize, command: bool) -> Result<(), ()>;
|
||||||
@ -24,16 +25,18 @@ impl<'a> ShiftRegisterPin<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutputPin for ShiftRegisterPin<'_> {
|
impl embedded_hal::digital::ErrorType for ShiftRegisterPin<'_> {
|
||||||
type Error = ();
|
type Error = Infallible;
|
||||||
|
}
|
||||||
|
|
||||||
fn set_low(&mut self) -> Result<(), Self::Error> {
|
impl OutputPin for ShiftRegisterPin<'_> {
|
||||||
self.shift_register.update(self.index, false)?;
|
fn set_low(&mut self) -> Result<(), Infallible> {
|
||||||
|
self.shift_register.update(self.index, false).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_high(&mut self) -> Result<(), Self::Error> {
|
fn set_high(&mut self) -> Result<(), Infallible> {
|
||||||
self.shift_register.update(self.index, true)?;
|
self.shift_register.update(self.index, true).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,12 +43,12 @@
|
|||||||
Tank Warn below mL
|
Tank Warn below mL
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="number" min="1" max="500000" id="tank_empty_mv">
|
<input type="number" min="0" max="100" id="tank_empty_percent">
|
||||||
Tank Empty Voltage (mv)
|
Tank Empty Percent (% max move)
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="number" min="1" max="500000" id="tank_full_mv">
|
<input type="number" min="0" max="100" id="tank_full_percent">
|
||||||
Tank Full Voltage (mv)
|
Tank Full Percent (% max move)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Light:</h3>
|
<h3>Light:</h3>
|
||||||
|
@ -5,10 +5,10 @@ use std::{
|
|||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::BOARD_ACCESS;
|
use crate::{espota::OtaUpdate, BOARD_ACCESS};
|
||||||
|
use core::result::Result::Ok;
|
||||||
use embedded_svc::http::Method;
|
use embedded_svc::http::Method;
|
||||||
use esp_idf_svc::http::server::{Configuration, EspHttpServer};
|
use esp_idf_svc::http::server::{Configuration, EspHttpServer};
|
||||||
use esp_ota::OtaUpdate;
|
|
||||||
use heapless::String;
|
use heapless::String;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ pub fn httpd_initial(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>>
|
|||||||
.fn_handler("/", Method::Get, move |request| {
|
.fn_handler("/", Method::Get, move |request| {
|
||||||
let mut response = request.into_ok_response()?;
|
let mut response = request.into_ok_response()?;
|
||||||
response.write(include_bytes!("initial_config.html"))?;
|
response.write(include_bytes!("initial_config.html"))?;
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ pub fn httpd_initial(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>>
|
|||||||
response.write(ssid_json.as_bytes())?;
|
response.write(ssid_json.as_bytes())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ pub fn httpd_initial(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>>
|
|||||||
request
|
request
|
||||||
.into_status_response(500)?
|
.into_status_response(500)?
|
||||||
.write(error_text.as_bytes())?;
|
.write(error_text.as_bytes())?;
|
||||||
return Ok(());
|
return anyhow::Ok(());
|
||||||
}
|
}
|
||||||
let actual_data = &buf[0..read.unwrap()];
|
let actual_data = &buf[0..read.unwrap()];
|
||||||
println!("raw {:?}", actual_data);
|
println!("raw {:?}", actual_data);
|
||||||
@ -75,14 +75,14 @@ pub fn httpd_initial(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>>
|
|||||||
request
|
request
|
||||||
.into_status_response(500)?
|
.into_status_response(500)?
|
||||||
.write(error_text.as_bytes())?;
|
.write(error_text.as_bytes())?;
|
||||||
return Ok(());
|
return anyhow::Ok(());
|
||||||
}
|
}
|
||||||
let mut board = BOARD_ACCESS.lock().unwrap();
|
let mut board = BOARD_ACCESS.lock().unwrap();
|
||||||
board.set_wifi(&wifi_config.unwrap())?;
|
board.set_wifi(&wifi_config.unwrap())?;
|
||||||
let mut response = request.into_status_response(202)?;
|
let mut response = request.into_status_response(202)?;
|
||||||
response.write("saved".as_bytes())?;
|
response.write("saved".as_bytes())?;
|
||||||
reboot_now.store(true, std::sync::atomic::Ordering::Relaxed);
|
reboot_now.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ pub fn httpd_initial(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>>
|
|||||||
.fn_handler("/boardtest", Method::Post, move |_| {
|
.fn_handler("/boardtest", Method::Post, move |_| {
|
||||||
let mut board = BOARD_ACCESS.lock().unwrap();
|
let mut board = BOARD_ACCESS.lock().unwrap();
|
||||||
board.test()?;
|
board.test()?;
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -104,14 +104,14 @@ pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
|
|||||||
.fn_handler("/", Method::Get, move |request| {
|
.fn_handler("/", Method::Get, move |request| {
|
||||||
let mut response = request.into_ok_response()?;
|
let mut response = request.into_ok_response()?;
|
||||||
response.write(include_bytes!("config.html"))?;
|
response.write(include_bytes!("config.html"))?;
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
server
|
server
|
||||||
.fn_handler("/get_config", Method::Get, move |request| {
|
.fn_handler("/get_config", Method::Get, move |request| {
|
||||||
let mut response = request.into_ok_response()?;
|
let mut response = request.into_ok_response()?;
|
||||||
let mut board = BOARD_ACCESS.lock()?;
|
let mut board = BOARD_ACCESS.lock().unwrap();
|
||||||
match board.get_config() {
|
match board.get_config() {
|
||||||
Ok(config) => {
|
Ok(config) => {
|
||||||
let config_json = serde_json::to_string(&config)?;
|
let config_json = serde_json::to_string(&config)?;
|
||||||
@ -122,7 +122,7 @@ pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
|
|||||||
response.write(config_json.as_bytes())?;
|
response.write(config_json.as_bytes())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
|
|||||||
request
|
request
|
||||||
.into_status_response(500)?
|
.into_status_response(500)?
|
||||||
.write(error_text.as_bytes())?;
|
.write(error_text.as_bytes())?;
|
||||||
return Ok(());
|
return anyhow::Ok(());
|
||||||
}
|
}
|
||||||
let actual_data = &buf[0..read.unwrap()];
|
let actual_data = &buf[0..read.unwrap()];
|
||||||
println!("Raw data {}", from_utf8(actual_data).unwrap());
|
println!("Raw data {}", from_utf8(actual_data).unwrap());
|
||||||
@ -171,21 +171,21 @@ pub fn shared() -> Box<EspHttpServer<'static>> {
|
|||||||
.fn_handler("/version", Method::Get, |request| {
|
.fn_handler("/version", Method::Get, |request| {
|
||||||
let mut response = request.into_ok_response()?;
|
let mut response = request.into_ok_response()?;
|
||||||
response.write(env!("VERGEN_GIT_DESCRIBE").as_bytes())?;
|
response.write(env!("VERGEN_GIT_DESCRIBE").as_bytes())?;
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
server
|
server
|
||||||
.fn_handler("/bundle.js", Method::Get, |request| {
|
.fn_handler("/bundle.js", Method::Get, |request| {
|
||||||
let mut response = request.into_ok_response()?;
|
let mut response = request.into_ok_response()?;
|
||||||
response.write(include_bytes!("bundle.js"))?;
|
response.write(include_bytes!("bundle.js"))?;
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
server
|
server
|
||||||
.fn_handler("/favicon.ico", Method::Get, |request| {
|
.fn_handler("/favicon.ico", Method::Get, |request| {
|
||||||
let mut response = request.into_ok_response()?;
|
let mut response = request.into_ok_response()?;
|
||||||
response.write(include_bytes!("favicon.ico"))?;
|
response.write(include_bytes!("favicon.ico"))?;
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
server
|
server
|
||||||
@ -196,7 +196,7 @@ pub fn shared() -> Box<EspHttpServer<'static>> {
|
|||||||
request
|
request
|
||||||
.into_status_response(500)?
|
.into_status_response(500)?
|
||||||
.write(error_text.as_bytes())?;
|
.write(error_text.as_bytes())?;
|
||||||
return Ok(());
|
return anyhow::Ok(());
|
||||||
}
|
}
|
||||||
let mut ota = ota.unwrap();
|
let mut ota = ota.unwrap();
|
||||||
println!("start ota");
|
println!("start ota");
|
||||||
|
@ -5,8 +5,8 @@ interface PlantConfig {
|
|||||||
tank_allow_pumping_if_sensor_error: boolean,
|
tank_allow_pumping_if_sensor_error: boolean,
|
||||||
tank_useable_ml: number,
|
tank_useable_ml: number,
|
||||||
tank_warn_percent: number,
|
tank_warn_percent: number,
|
||||||
tank_empty_mv: number,
|
tank_empty_percent: number,
|
||||||
tank_full_mv: number,
|
tank_full_percent: number,
|
||||||
night_lamp_hour_start: number,
|
night_lamp_hour_start: number,
|
||||||
night_lamp_hour_end: number,
|
night_lamp_hour_end: number,
|
||||||
night_lamp_only_when_dark: boolean,
|
night_lamp_only_when_dark: boolean,
|
||||||
@ -46,10 +46,10 @@ let fromWrapper = (() => {
|
|||||||
|
|
||||||
let tank_useable_ml = document.getElementById("tank_useable_ml") as HTMLInputElement;
|
let tank_useable_ml = document.getElementById("tank_useable_ml") as HTMLInputElement;
|
||||||
tank_useable_ml.onchange = updateJson
|
tank_useable_ml.onchange = updateJson
|
||||||
let tank_empty_mv = document.getElementById("tank_empty_mv") as HTMLInputElement;
|
let tank_empty_percent = document.getElementById("tank_empty_percent") as HTMLInputElement;
|
||||||
tank_empty_mv.onchange = updateJson
|
tank_empty_percent.onchange = updateJson
|
||||||
let tank_full_mv = document.getElementById("tank_full_mv") as HTMLInputElement;
|
let tank_full_percent = document.getElementById("tank_full_percent") as HTMLInputElement;
|
||||||
tank_full_mv.onchange = updateJson
|
tank_full_percent.onchange = updateJson
|
||||||
let tank_warn_percent = document.getElementById("tank_warn_percent") as HTMLInputElement;
|
let tank_warn_percent = document.getElementById("tank_warn_percent") as HTMLInputElement;
|
||||||
tank_warn_percent.onchange = updateJson
|
tank_warn_percent.onchange = updateJson
|
||||||
let tank_sensor_enabled = document.getElementById("tank_sensor_enabled") as HTMLInputElement;
|
let tank_sensor_enabled = document.getElementById("tank_sensor_enabled") as HTMLInputElement;
|
||||||
@ -189,8 +189,8 @@ let fromWrapper = (() => {
|
|||||||
tank_allow_pumping_if_sensor_error.checked = current.tank_allow_pumping_if_sensor_error;
|
tank_allow_pumping_if_sensor_error.checked = current.tank_allow_pumping_if_sensor_error;
|
||||||
tank_useable_ml.value = current.tank_useable_ml.toString();
|
tank_useable_ml.value = current.tank_useable_ml.toString();
|
||||||
tank_warn_percent.value = current.tank_warn_percent.toString();
|
tank_warn_percent.value = current.tank_warn_percent.toString();
|
||||||
tank_empty_mv.value = current.tank_empty_mv.toString();
|
tank_empty_percent.value = current.tank_empty_percent.toString();
|
||||||
tank_full_mv.value = current.tank_full_mv.toString();
|
tank_full_percent.value = current.tank_full_percent.toString();
|
||||||
|
|
||||||
night_lamp_time_start.value = current.night_lamp_hour_start.toString();
|
night_lamp_time_start.value = current.night_lamp_hour_start.toString();
|
||||||
night_lamp_time_end.value = current.night_lamp_hour_end.toString();
|
night_lamp_time_end.value = current.night_lamp_hour_end.toString();
|
||||||
@ -220,8 +220,8 @@ let fromWrapper = (() => {
|
|||||||
tank_sensor_enabled: tank_sensor_enabled.checked,
|
tank_sensor_enabled: tank_sensor_enabled.checked,
|
||||||
tank_useable_ml: +tank_useable_ml.value,
|
tank_useable_ml: +tank_useable_ml.value,
|
||||||
tank_warn_percent: +tank_warn_percent.value,
|
tank_warn_percent: +tank_warn_percent.value,
|
||||||
tank_empty_mv: +tank_empty_mv.value,
|
tank_empty_percent: +tank_empty_percent.value,
|
||||||
tank_full_mv: +tank_full_mv.value,
|
tank_full_percent: +tank_full_percent.value,
|
||||||
night_lamp_hour_start: +night_lamp_time_start.value,
|
night_lamp_hour_start: +night_lamp_time_start.value,
|
||||||
night_lamp_hour_end: +night_lamp_time_end.value,
|
night_lamp_hour_end: +night_lamp_time_end.value,
|
||||||
night_lamp_only_when_dark: night_lamp_only_when_dark.checked,
|
night_lamp_only_when_dark: night_lamp_only_when_dark.checked,
|
||||||
|
Loading…
Reference in New Issue
Block a user