Compare commits

...

10 Commits

Author SHA1 Message Date
680d1c3aaf fix rtc storage 2024-02-15 23:00:05 +01:00
060a1cc32d proceed with bq34z100 extraction 2024-02-02 21:35:04 +01:00
541f7e4471 Also ignore rust folder 2024-01-25 13:31:44 +01:00
7ea1486e2c made most battery code ready to work 2024-01-22 23:13:52 +01:00
b933516062 fix for bq34z100 led mode, added i2c header 2024-01-22 23:13:37 +01:00
b533739aa4 bq34z100 code improvements 2024-01-21 06:11:06 +01:00
12463c557b add i2c initial and pull configs 2024-01-17 21:25:01 +01:00
9473466feb set mppt output voltage to be less agressive 2024-01-10 19:49:57 +01:00
b5b5b25238 main pump handler 2024-01-09 00:16:13 +01:00
b9ec3247af clippy 2024-01-07 14:34:45 +01:00
16 changed files with 16153 additions and 12722 deletions

2
.gitignore vendored
View File

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

View File

@ -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": [],

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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 doesnt 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
),
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -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");

View File

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