33 Commits

Author SHA1 Message Date
cfe23c8a09 mqtt via mcutie 2025-09-29 01:00:11 +02:00
3d18b0dbf6 split webserver into submodules 2025-09-28 19:08:43 +02:00
7ebc147f51 v3 wip 2025-09-28 13:42:30 +02:00
f0bda32d7a use rom linked functions for pin keeping 2025-09-26 23:32:41 +02:00
76f59b093d enable deepsleep and gpio1 + timer wake 2025-09-26 22:07:48 +02:00
7fc8d0c882 sta config mode 2025-09-26 01:13:08 +02:00
6d5bb5b966 sntp and wifi sta mode working 2025-09-26 00:44:40 +02:00
336961f0a0 not working wip of station mode 2025-09-25 22:32:10 +02:00
e20b474dfd bring selftest online, bring tz online, fix set_config/get_config race 2025-09-24 22:29:58 +02:00
5b009f50e5 tanksensor and rtc sync 2025-09-23 22:59:08 +02:00
d010c5d12a get more functions online 2025-09-23 20:51:59 +02:00
b594a02870 fix progress display 2025-09-23 00:43:19 +02:00
1f3349c348 add pump expander 2025-09-23 00:26:05 +02:00
5b0e2b6797 i2c working, rtc working, eeprom working 2025-09-22 23:44:33 +02:00
1791f463b7 remove anyhow 2025-09-22 01:49:25 +02:00
c94f5bdb45 get log to work, make time accessible 2025-09-20 11:31:51 +02:00
584d6df2d0 file up & download and delete 2025-09-18 01:39:32 +02:00
cd63e76469 set read config initial somewhat ready 2025-09-17 03:50:21 +02:00
4c54edbcea littlefs2 impl stuff 2025-09-17 01:36:53 +02:00
8b938e7687 small adjustments 2025-09-16 22:41:45 +02:00
1c84cd00da rework wifi controller share 2025-09-16 02:24:03 +02:00
1397f5d775 keep ota around for alter queries to it 2025-09-16 01:41:38 +02:00
65f6670ca4 adda lot of basic webserver back 2025-09-15 01:21:50 +02:00
049a9d027c use ssid 2025-09-14 13:50:46 +02:00
4aa25c687b minimal startup running 2025-09-14 13:19:30 +02:00
b3cc313139 initial webserver stub running 2025-09-14 05:29:23 +02:00
be3c4a5095 startup and ota state detection working, now wifi_ap next 2025-09-13 20:56:11 +02:00
4160202cdc more changes and linking 2025-09-13 02:47:53 +02:00
9de85b6e37 it's alive 2025-09-13 01:39:47 +02:00
79087c9353 more async migration 2025-09-12 16:30:35 +02:00
0d495d0f56 probalby found most of the import related stuff, now the real refactor begins 2025-09-12 00:05:52 +02:00
242e748ca0 started cleanup and moving from std to no_std 2025-09-11 22:47:11 +02:00
f853b6f2b2 removed all std dependencies … WIP move to embassy pure rust code 2025-09-11 21:42:17 +02:00
40 changed files with 5528 additions and 3725 deletions

View File

@@ -460,6 +460,7 @@
"single_global_label": "ignore", "single_global_label": "ignore",
"unannotated": "error", "unannotated": "error",
"unconnected_wire_endpoint": "warning", "unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error", "unit_value_mismatch": "error",
"unresolved_variable": "error", "unresolved_variable": "error",
"wire_dangling": "error" "wire_dangling": "error"

View File

@@ -1,26 +1,31 @@
[build] [build]
#target = "xtensa-esp32-espidf" rustflags = [
target = "riscv32imac-esp-espidf" # Required to obtain backtraces (e.g. when using the "esp-backtrace" crate.)
# NOTE: May negatively impact performance of produced code
"-C", "force-frame-pointers",
"-Z", "stack-protector=all",
"-C", "link-arg=-Tlinkall.x",
]
[target.riscv32imac-esp-espidf] target = "riscv32imac-unknown-none-elf"
linker = "ldproxy"
[target.riscv32imac-unknown-none-elf]
runner = "espflash flash --monitor --chip esp32c6 --baud 921600 --partition-table partitions.csv"
#runner = "espflash flash --monitor --baud 921600 --partition-table partitions.csv -b no-reset" # Select this runner in case of usb ttl #runner = "espflash flash --monitor --baud 921600 --partition-table partitions.csv -b no-reset" # Select this runner in case of usb ttl
runner = "espflash flash --monitor" #runner = "espflash flash --monitor"
#runner = "cargo runner" #runner = "cargo runner"
#runner = "espflash flash --monitor --partition-table partitions.csv -b no-reset" # create upgrade image file for webupload #runner = "espflash flash --monitor --partition-table partitions.csv -b no-reset" # create upgrade image file for webupload
# runner = espflash erase-parts otadata //ensure flash is clean # runner = espflash erase-parts otadata //ensure flash is clean
rustflags = ["--cfg", "espidf_time64"] # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110
[unstable]
build-std = ["std", "panic_abort"]
[env] [env]
MCU = "esp32c6"
# Note: this variable is not used by the pio builder (`cargo build --features pio`)
ESP_IDF_VERSION = "v5.2.1"
CHRONO_TZ_TIMEZONE_FILTER = "UTC|America/New_York|America/Chicago|America/Los_Angeles|Europe/London|Europe/Berlin|Europe/Paris|Asia/Tokyo|Asia/Shanghai|Asia/Kolkata|Australia/Sydney|America/Sao_Paulo|Africa/Johannesburg|Asia/Dubai|Pacific/Auckland" CHRONO_TZ_TIMEZONE_FILTER = "UTC|America/New_York|America/Chicago|America/Los_Angeles|Europe/London|Europe/Berlin|Europe/Paris|Asia/Tokyo|Asia/Shanghai|Asia/Kolkata|Australia/Sydney|America/Sao_Paulo|Africa/Johannesburg|Asia/Dubai|Pacific/Auckland"
CARGO_WORKSPACE_DIR = { value = "", relative = true } CARGO_WORKSPACE_DIR = { value = "", relative = true }
RUST_BACKTRACE = "full" ESP_LOG = "info"
[unstable]
build-std = ["alloc", "core"]

View File

@@ -1,14 +1,18 @@
<component name="ProjectDictionaryState"> <component name="ProjectDictionaryState">
<dictionary name="project"> <dictionary name="project">
<words> <words>
<w>boardtest</w>
<w>buildtime</w> <w>buildtime</w>
<w>deepsleep</w> <w>deepsleep</w>
<w>githash</w> <w>githash</w>
<w>lamptest</w>
<w>lightstate</w> <w>lightstate</w>
<w>mppt</w> <w>mppt</w>
<w>plantstate</w> <w>plantstate</w>
<w>pumptest</w>
<w>sntp</w> <w>sntp</w>
<w>vergen</w> <w>vergen</w>
<w>wifiscan</w>
</words> </words>
</dictionary> </dictionary>
</component> </component>

View File

@@ -7,5 +7,6 @@
</Languages> </Languages>
</inspection_tool> </inspection_tool>
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NewCrateVersionAvailable" enabled="true" level="INFORMATION" enabled_by_default="true" />
</profile> </profile>
</component> </component>

View File

@@ -1,31 +1,17 @@
[package] [package]
name = "plant-ctrl2"
version = "0.1.0"
authors = ["Empire Phoenix"]
edition = "2021" edition = "2021"
resolver = "2" name = "plant-ctrl2"
#rust-version = "1.71" rust-version = "1.86"
version = "0.1.0"
[profile.dev] # Explicitly configure the binary target and disable building it as a test/bench.
# Explicitly disable LTO which the Xtensa codegen backend has issues [[bin]]
lto = false name = "plant-ctrl2"
strip = false path = "src/main.rs"
debug = true # Prevent IDEs/Cargo from trying to compile a test harness for this no_std binary.
overflow-checks = true test = false
panic = "abort" bench = false
incremental = true doc = false
opt-level = 2
[profile.release]
# Explicitly disable LTO which the Xtensa codegen backend has issues
lto = false
strip = true
debug = false
overflow-checks = false
panic = "abort"
incremental = true
opt-level = "z"
[package.metadata.cargo_runner] [package.metadata.cargo_runner]
# The string `$TARGET_FILE` will be replaced with the path from cargo. # The string `$TARGET_FILE` will be replaced with the path from cargo.
@@ -40,74 +26,146 @@ command = [
"partitions.csv" "partitions.csv"
] ]
#this strips the bootloader, we need that tho
#strip = true
[profile.dev]
lto = "fat"
debug = false
overflow-checks = true
panic = "abort"
incremental = true
opt-level = "z"
[profile.release]
lto = "fat"
#debug = false
overflow-checks = true
panic = "abort"
incremental = false
opt-level = "z"
[package.metadata.espflash] [package.metadata.espflash]
partition_table = "partitions.csv" partition_table = "partitions.csv"
[features]
default = ["std", "esp-idf-svc/native"]
pio = ["esp-idf-svc/pio"]
std = ["alloc", "esp-idf-svc/binstart", "esp-idf-svc/std"]
alloc = ["esp-idf-svc/alloc"]
nightly = ["esp-idf-svc/nightly"]
experimental = ["esp-idf-svc/experimental"]
#embassy = ["esp-idf-svc/embassy-sync", "esp-idf-svc/critical-section", "esp-idf-svc/embassy-time-driver"]
[dependencies] [dependencies]
#ESP stuff #ESP stuff
embedded-svc = { version = "0.28.1", features = ["experimental"] } esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32c6"] }
esp-idf-hal = "0.45.2" esp-hal = { version = "=1.0.0-rc.0", features = [
esp-idf-sys = { version = "0.36.1", features = ["binstart", "native"] } "esp32c6",
esp-idf-svc = { version = "0.51.0", default-features = false } "log-04",
"unstable",
"rt"
] }
log = "0.4.27"
embassy-net = { version = "0.7.1", default-features = false, features = [
"dhcpv4",
"log",
"medium-ethernet",
"tcp",
"udp",
"proto-ipv4",
"dns"
] }
embedded-io = "0.6.1"
embedded-io-async = "0.6.1"
esp-alloc = "0.8.0"
esp-backtrace = { version = "0.17.0", features = [
"esp32c6",
"exception-handler",
"panic-handler",
"println",
"colors",
"custom-halt"
] }
esp-println = { version = "0.15.0", features = ["esp32c6", "log-04"] }
# for more networking protocol support see https://crates.io/crates/edge-net
embassy-executor = { version = "0.7.0", features = [
"log",
"task-arena-size-64",
"nightly"
] }
embassy-time = { version = "0.5.0", features = ["log"], default-features = false }
esp-hal-embassy = { version = "0.9.0", features = ["esp32c6", "log-04"] }
esp-storage = { version = "0.7.0", features = ["esp32c6"] }
esp-wifi = { version = "0.15.0", features = [
"builtin-scheduler",
"esp-alloc",
"esp32c6",
"log-04",
"smoltcp",
"wifi",
] }
smoltcp = { version = "0.12.0", default-features = false, features = [
"alloc",
"log",
"medium-ethernet",
"multicast",
"proto-dhcpv4",
"proto-ipv6",
"proto-dns",
"proto-ipv4",
"socket-dns",
"socket-icmp",
"socket-raw",
"socket-tcp",
"socket-udp",
] }
#static_cell = "2.1.1"
embedded-hal = "1.0.0" embedded-hal = "1.0.0"
heapless = { version = "0.8", features = ["serde"] } embedded-hal-bus = { version = "0.3.0" }
embedded-hal-bus = { version = "0.3.0", features = ["std"] }
#Hardware additional driver #Hardware additional driver
ds18b20 = "0.1.1"
bq34z100 = { version = "0.3.0", features = ["flashstream"] } #bq34z100 = { version = "0.3.0", default-features = false }
one-wire-bus = "0.1.1" onewire = "0.4.0"
#strum = { version = "0.27.0", default-feature = false, features = ["derive"] }
measurements = "0.11.0"
ds323x = "0.6.0" ds323x = "0.6.0"
#pure code dependencies
once_cell = "1.19.0"
anyhow = { version = "1.0.75", features = ["std", "backtrace"] }
strum = { version = "0.27.0", features = ["derive"] }
measurements = "0.11.0"
#json #json
serde = { version = "1.0.192", features = ["derive"] } serde = { version = "1.0.219", features = ["derive", "alloc"], default-features = false }
serde_json = "1.0.108" serde_json = { version = "1.0.143", default-features = false, features = ["alloc"] }
#timezone chrono = { version = "0.4.42", default-features = false, features = ["iana-time-zone", "alloc", "serde"] }
chrono-tz = { version = "0.10.4", default-features = false, features = ["filter-by-regex"] }
chrono = { version = "0.4.23", default-features = false, features = ["iana-time-zone", "alloc", "serde"] }
chrono-tz = { version = "0.10.3", default-features = false, features = ["filter-by-regex"] }
eeprom24x = "0.7.2" eeprom24x = "0.7.2"
url = "2.5.3"
crc = "3.2.1" crc = "3.2.1"
bincode = "2.0.1"
ringbuffer = "0.15.0"
text-template = "0.1.0"
strum_macros = "0.27.0" strum_macros = "0.27.0"
esp-ota = { version = "0.2.2", features = ["log"] }
unit-enum = "1.4.1" unit-enum = "1.4.1"
pca9535 = { version = "2.0.0", features = ["std"] } pca9535 = { version = "2.0.0" }
ina219 = { version = "0.2.0", features = ["std"] } ina219 = { version = "0.2.0" }
embedded-storage = "=0.3.1" embedded-storage = "=0.3.1"
ekv = "1.0.0" portable-atomic = "1.11.1"
embedded-can = "0.4.1" embassy-sync = { version = "0.7.2", features = ["log"] }
async-trait = "0.1.89"
bq34z100 = { version = "0.4.0", default-features = false }
edge-dhcp = "0.6.0"
edge-nal = "0.5.0"
edge-nal-embassy = "0.6.0"
static_cell = "2.1.1"
edge-http = { version = "0.6.1", features = ["log"] }
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] }
littlefs2-core = "0.1.1"
bytemuck = { version = "1.23.2", features = ["derive", "min_const_generics", "pod_saturating", "extern_crate_alloc"] }
deranged = "0.5.3"
embassy-embedded-hal = "0.5.0"
bincode = { version = "2.0.1", default-features = false, features = ["derive"] }
sntpc = { version = "0.6.0", default-features = false, features = ["log", "embassy-socket", "embassy-socket-ipv6"] }
option-lock = { version = "0.3.1", default-features = false }
#stay in sync with mcutie version here!
heapless = { version = "0.7.17", features = ["serde"] }
mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] }
[patch.crates-io] [patch.crates-io]
#esp-idf-hal = { git = "https://github.com/esp-rs/esp-idf-hal.git" } mcutie = { git = 'https://github.com/empirephoenix/mcutie.git' }
#esp-idf-hal = { git = "https://github.com/empirephoenix/esp-idf-hal.git" }
#esp-idf-sys = { git = "https://github.com/empirephoenix/esp-idf-sys.git" }
#esp-idf-sys = { git = "https://github.com/esp-rs/esp-idf-sys.git" }
#esp-idf-svc = { git = "https://github.com/esp-rs/esp-idf-svc.git" }
#bq34z100 = { path = "../../bq34z100_rust" } #bq34z100 = { path = "../../bq34z100_rust" }
[build-dependencies] [build-dependencies]
cc = "=1.1.30"
embuild = { version = "0.32.0", features = ["espidf"] }
vergen = { version = "8.2.6", features = ["build", "git", "gitcl"] } vergen = { version = "8.2.6", features = ["build", "git", "gitcl"] }

View File

@@ -1,10 +1,64 @@
use std::process::Command; use std::process::Command;
use vergen::EmitBuilder; use vergen::EmitBuilder;
fn linker_be_nice() {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
let kind = &args[1];
let what = &args[2];
match kind.as_str() {
"undefined-symbol" => match what.as_str() {
"_defmt_timestamp" => {
eprintln!();
eprintln!("💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`");
eprintln!();
}
"_stack_start" => {
eprintln!();
eprintln!("💡 Is the linker script `linkall.x` missing?");
eprintln!();
}
"esp_wifi_preempt_enable"
| "esp_wifi_preempt_yield_task"
| "esp_wifi_preempt_task_create" => {
eprintln!();
eprintln!("💡 `esp-wifi` has no scheduler enabled. Make sure you have the `builtin-scheduler` feature enabled, or that you provide an external scheduler.");
eprintln!();
}
"embedded_test_linker_file_not_added_to_rustflags" => {
eprintln!();
eprintln!("💡 `embedded-test` not found - make sure `embedded-test.x` is added as a linker script for tests");
eprintln!();
}
_ => (),
},
// we don't have anything helpful for "missing-lib" yet
_ => {
std::process::exit(1);
}
}
std::process::exit(0);
}
println!(
"cargo:rustc-link-arg=--error-handling-script={}",
std::env::current_exe().unwrap().display()
);
}
fn main() { fn main() {
println!("cargo:rerun-if-changed=./src/src_webpack"); webpack();
linker_be_nice();
let _ = EmitBuilder::builder().all_git().all_build().emit();
}
fn webpack() {
//println!("cargo:rerun-if-changed=./src/src_webpack");
Command::new("rm") Command::new("rm")
.arg("./src/webserver/bundle.js") .arg("./src/webserver/bundle.js.gz")
.output() .output()
.unwrap(); .unwrap();
@@ -27,14 +81,14 @@ fn main() {
let _ = Command::new("cmd") let _ = Command::new("cmd")
.arg("/K") .arg("/K")
.arg("move") .arg("move")
.arg("./src_webpack/bundle.js") .arg("./src_webpack/bundle.js.gz")
.arg("./src/webserver") .arg("./src/webserver")
.output() .output()
.unwrap(); .unwrap();
let _ = Command::new("cmd") let _ = Command::new("cmd")
.arg("/K") .arg("/K")
.arg("move") .arg("move")
.arg("./src_webpack/index.html") .arg("./src_webpack/index.html.gz")
.arg("./src/webserver") .arg("./src/webserver")
.output() .output()
.unwrap(); .unwrap();
@@ -53,18 +107,15 @@ fn main() {
// move webpack results to rust webserver src // move webpack results to rust webserver src
let _ = Command::new("mv") let _ = Command::new("mv")
.arg("./src_webpack/bundle.js") .arg("./src_webpack/bundle.js.gz")
.arg("./src/webserver") .arg("./src/webserver")
.output() .output()
.unwrap(); .unwrap();
let _ = Command::new("mv") let _ = Command::new("mv")
.arg("./src_webpack/index.html") .arg("./src_webpack/index.html.gz")
.arg("./src/webserver") .arg("./src/webserver")
.output() .output()
.unwrap(); .unwrap();
} }
} }
embuild::espidf::sysenv::output();
let _ = EmitBuilder::builder().all_git().all_build().emit();
} }

View File

@@ -1,5 +1,8 @@
partition_table = "partitions.csv"
[connection] [connection]
serial = "/dev/ttyACM0"
[[usb_device]]
vid = "303a"
pid = "1001"
[flash] [flash]
size = "16MB" size = "16MB"

View File

@@ -3,4 +3,4 @@ otadata, data, ota, , 8k,
phy_init, data, phy, , 4k, phy_init, data, phy, , 4k,
ota_0, app, ota_0, , 3968k, ota_0, app, ota_0, , 3968k,
ota_1, app, ota_1, , 3968k, ota_1, app, ota_1, , 3968k,
storage, data, spiffs, , 8M, storage, data, littlefs,, 8M,
1 nvs data nvs 16k
3 phy_init data phy 4k
4 ota_0 app ota_0 3968k
5 ota_1 app ota_1 3968k
6 storage data spiffs littlefs 8M

View File

@@ -1,3 +1,2 @@
[toolchain] [toolchain]
channel = "nightly" channel = "nightly"
toolchain = "esp"

View File

@@ -1,7 +1,8 @@
use crate::hal::PLANT_COUNT; use crate::hal::PLANT_COUNT;
use crate::plant_state::PlantWateringMode; use crate::plant_state::PlantWateringMode;
use alloc::string::String;
use core::str::FromStr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(default)] #[serde(default)]
@@ -9,10 +10,10 @@ pub struct NetworkConfig {
pub ap_ssid: heapless::String<32>, pub ap_ssid: heapless::String<32>,
pub ssid: Option<heapless::String<32>>, pub ssid: Option<heapless::String<32>>,
pub password: Option<heapless::String<64>>, pub password: Option<heapless::String<64>>,
pub mqtt_url: Option<heapless::String<128>>, pub mqtt_url: Option<String>,
pub base_topic: Option<heapless::String<64>>, pub base_topic: Option<heapless::String<64>>,
pub mqtt_user: Option<heapless::String<32>>, pub mqtt_user: Option<String>,
pub mqtt_password: Option<heapless::String<64>>, pub mqtt_password: Option<String>,
pub max_wait: u32, pub max_wait: u32,
} }
impl Default for NetworkConfig { impl Default for NetworkConfig {

265
rust/src/fat_error.rs Normal file
View File

@@ -0,0 +1,265 @@
use alloc::format;
use alloc::string::{String, ToString};
use core::convert::Infallible;
use core::fmt;
use core::str::Utf8Error;
use embassy_embedded_hal::shared_bus::I2cDeviceError;
use embassy_executor::SpawnError;
use embassy_sync::mutex::TryLockError;
use esp_hal::i2c::master::ConfigError;
use esp_wifi::wifi::WifiError;
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
use littlefs2_core::PathError;
use onewire::Error;
use pca9535::ExpanderError;
//All error superconstruct
#[derive(Debug)]
pub enum FatError {
OneWireError {
error: Error<Infallible>,
},
String {
error: String,
},
LittleFSError {
error: littlefs2_core::Error,
},
PathError {
error: PathError,
},
TryLockError {
error: TryLockError,
},
WifiError {
error: WifiError,
},
SerdeError {
error: serde_json::Error,
},
PreconditionFailed {
error: String,
},
NoBatteryMonitor,
SpawnError {
error: SpawnError,
},
PartitionError {
error: esp_bootloader_esp_idf::partitions::Error,
},
I2CConfigError {
error: ConfigError,
},
DS323 {
error: String,
},
Eeprom24x {
error: String,
},
ExpanderError {
error: String,
},
}
pub type FatResult<T> = Result<T, FatError>;
impl fmt::Display for FatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FatError::SpawnError { error } => {
write!(f, "SpawnError {:?}", error.to_string())
}
FatError::OneWireError { error } => write!(f, "OneWireError {:?}", error),
FatError::String { error } => write!(f, "{}", error),
FatError::LittleFSError { error } => write!(f, "LittleFSError {:?}", error),
FatError::PathError { error } => write!(f, "PathError {:?}", error),
FatError::TryLockError { error } => write!(f, "TryLockError {:?}", error),
FatError::WifiError { error } => write!(f, "WifiError {:?}", error),
FatError::SerdeError { error } => write!(f, "SerdeError {:?}", error),
FatError::PreconditionFailed { error } => write!(f, "PreconditionFailed {:?}", error),
FatError::PartitionError { error } => {
write!(f, "PartitionError {:?}", error)
}
FatError::NoBatteryMonitor => {
write!(f, "No Battery Monitor")
}
FatError::I2CConfigError { error } => write!(f, "I2CConfigError {:?}", error),
FatError::DS323 { error } => write!(f, "DS323 {:?}", error),
FatError::Eeprom24x { error } => write!(f, "Eeprom24x {:?}", error),
FatError::ExpanderError { error } => write!(f, "ExpanderError {:?}", error),
}
}
}
#[macro_export]
macro_rules! bail {
($msg:literal $(,)?) => {
return $crate::fat_error::fat_bail($msg)
};
($fmt:literal, $($arg:tt)*) => {
return $crate::fat_error::fat_bail(&alloc::format!($fmt, $($arg)*))
};
}
pub fn fat_bail<X>(message: &str) -> Result<X, FatError> {
Err(FatError::String {
error: message.to_string(),
})
}
pub trait ContextExt<T> {
fn context<C>(self, context: C) -> Result<T, FatError>
where
C: AsRef<str>;
}
impl<T> ContextExt<T> for Option<T> {
fn context<C>(self, context: C) -> Result<T, FatError>
where
C: AsRef<str>,
{
match self {
Some(value) => Ok(value),
None => Err(FatError::PreconditionFailed {
error: context.as_ref().to_string(),
}),
}
}
}
impl From<Error<Infallible>> for FatError {
fn from(error: Error<Infallible>) -> Self {
FatError::OneWireError { error }
}
}
impl From<littlefs2_core::Error> for FatError {
fn from(value: littlefs2_core::Error) -> Self {
FatError::LittleFSError { error: value }
}
}
impl From<PathError> for FatError {
fn from(value: PathError) -> Self {
FatError::PathError { error: value }
}
}
impl From<TryLockError> for FatError {
fn from(value: TryLockError) -> Self {
FatError::TryLockError { error: value }
}
}
impl From<WifiError> for FatError {
fn from(value: WifiError) -> Self {
FatError::WifiError { error: value }
}
}
impl From<serde_json::error::Error> for FatError {
fn from(value: serde_json::Error) -> Self {
FatError::SerdeError { error: value }
}
}
impl From<SpawnError> for FatError {
fn from(value: SpawnError) -> Self {
FatError::SpawnError { error: value }
}
}
impl From<esp_bootloader_esp_idf::partitions::Error> for FatError {
fn from(value: esp_bootloader_esp_idf::partitions::Error) -> Self {
FatError::PartitionError { error: value }
}
}
impl From<Utf8Error> for FatError {
fn from(value: Utf8Error) -> Self {
FatError::String {
error: value.to_string(),
}
}
}
impl<E: core::fmt::Debug> From<edge_http::io::Error<E>> for FatError {
fn from(value: edge_http::io::Error<E>) -> Self {
FatError::String {
error: format!("{:?}", value),
}
}
}
impl<E: core::fmt::Debug> From<ds323x::Error<E>> for FatError {
fn from(value: ds323x::Error<E>) -> Self {
FatError::DS323 {
error: format!("{:?}", value),
}
}
}
impl<E: core::fmt::Debug> From<eeprom24x::Error<E>> for FatError {
fn from(value: eeprom24x::Error<E>) -> Self {
FatError::Eeprom24x {
error: format!("{:?}", value),
}
}
}
impl<E: core::fmt::Debug> From<ExpanderError<I2cDeviceError<E>>> for FatError {
fn from(value: ExpanderError<I2cDeviceError<E>>) -> Self {
FatError::ExpanderError {
error: format!("{:?}", value),
}
}
}
impl From<bincode::error::DecodeError> for FatError {
fn from(value: bincode::error::DecodeError) -> Self {
FatError::Eeprom24x {
error: format!("{:?}", value),
}
}
}
impl From<bincode::error::EncodeError> for FatError {
fn from(value: bincode::error::EncodeError) -> Self {
FatError::Eeprom24x {
error: format!("{:?}", value),
}
}
}
impl From<ConfigError> for FatError {
fn from(value: ConfigError) -> Self {
FatError::I2CConfigError { error: value }
}
}
impl<E: core::fmt::Debug> From<I2cDeviceError<E>> for FatError {
fn from(value: I2cDeviceError<E>) -> Self {
FatError::String {
error: format!("{:?}", value),
}
}
}
impl<E: core::fmt::Debug> From<BusVoltageReadError<I2cDeviceError<E>>> for FatError {
fn from(value: BusVoltageReadError<I2cDeviceError<E>>) -> Self {
FatError::String {
error: format!("{:?}", value),
}
}
}
impl<E: core::fmt::Debug> From<ShuntVoltageReadError<I2cDeviceError<E>>> for FatError {
fn from(value: ShuntVoltageReadError<I2cDeviceError<E>>) -> Self {
FatError::String {
error: format!("{:?}", value),
}
}
}
impl From<Infallible> for FatError {
fn from(value: Infallible) -> Self {
panic!("Infallible error: {:?}", value)
}
}

View File

@@ -1,22 +1,28 @@
use anyhow::anyhow; use crate::fat_error::{FatError, FatResult};
use bq34z100::{Bq34Z100Error, Bq34z100g1, Bq34z100g1Driver}; use crate::hal::Box;
use embedded_hal_bus::i2c::MutexDevice; use alloc::string::String;
use esp_idf_hal::delay::Delay; use async_trait::async_trait;
use esp_idf_hal::i2c::{I2cDriver, I2cError}; use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags};
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use esp_hal::delay::Delay;
use esp_hal::i2c::master::I2c;
use esp_hal::Blocking;
use measurements::Temperature; use measurements::Temperature;
use serde::Serialize; use serde::Serialize;
#[async_trait]
pub trait BatteryInteraction { pub trait BatteryInteraction {
fn state_charge_percent(&mut self) -> Result<f32, BatteryError>; async fn state_charge_percent(&mut self) -> FatResult<f32>;
fn remaining_milli_ampere_hour(&mut self) -> Result<u16, BatteryError>; async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16>;
fn max_milli_ampere_hour(&mut self) -> Result<u16, BatteryError>; async fn max_milli_ampere_hour(&mut self) -> FatResult<u16>;
fn design_milli_ampere_hour(&mut self) -> Result<u16, BatteryError>; async fn design_milli_ampere_hour(&mut self) -> FatResult<u16>;
fn voltage_milli_volt(&mut self) -> Result<u16, BatteryError>; async fn voltage_milli_volt(&mut self) -> FatResult<u16>;
fn average_current_milli_ampere(&mut self) -> Result<i16, BatteryError>; async fn average_current_milli_ampere(&mut self) -> FatResult<i16>;
fn cycle_count(&mut self) -> Result<u16, BatteryError>; async fn cycle_count(&mut self) -> FatResult<u16>;
fn state_health_percent(&mut self) -> Result<u16, BatteryError>; async fn state_health_percent(&mut self) -> FatResult<u16>;
fn bat_temperature(&mut self) -> Result<u16, BatteryError>; async fn bat_temperature(&mut self) -> FatResult<u16>;
fn get_battery_state(&mut self) -> Result<BatteryState, BatteryError>; async fn get_battery_state(&mut self) -> FatResult<BatteryState>;
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@@ -37,14 +43,6 @@ pub enum BatteryError {
CommunicationError(String), CommunicationError(String),
} }
impl From<Bq34Z100Error<esp_idf_hal::i2c::I2cError>> for BatteryError {
fn from(err: Bq34Z100Error<esp_idf_hal::i2c::I2cError>) -> Self {
BatteryError::CommunicationError(
anyhow!("failed to communicate with battery monitor: {:?}", err).to_string(),
)
}
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub enum BatteryState { pub enum BatteryState {
Unknown, Unknown,
@@ -53,45 +51,46 @@ pub enum BatteryState {
/// If no battery monitor is installed this implementation will be used /// If no battery monitor is installed this implementation will be used
pub struct NoBatteryMonitor {} pub struct NoBatteryMonitor {}
#[async_trait]
impl BatteryInteraction for NoBatteryMonitor { impl BatteryInteraction for NoBatteryMonitor {
fn state_charge_percent(&mut self) -> Result<f32, BatteryError> { async fn state_charge_percent(&mut self) -> FatResult<f32> {
Err(BatteryError::NoBatteryMonitor) // No monitor configured: assume full battery for lightstate logic
Ok(100.0)
} }
fn remaining_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> { async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(BatteryError::NoBatteryMonitor) Err(FatError::NoBatteryMonitor)
} }
fn max_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> { async fn max_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(BatteryError::NoBatteryMonitor) Err(FatError::NoBatteryMonitor)
} }
fn design_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> { async fn design_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(BatteryError::NoBatteryMonitor) Err(FatError::NoBatteryMonitor)
} }
fn voltage_milli_volt(&mut self) -> Result<u16, BatteryError> { async fn voltage_milli_volt(&mut self) -> FatResult<u16> {
Err(BatteryError::NoBatteryMonitor) Err(FatError::NoBatteryMonitor)
} }
fn average_current_milli_ampere(&mut self) -> Result<i16, BatteryError> { async fn average_current_milli_ampere(&mut self) -> FatResult<i16> {
Err(BatteryError::NoBatteryMonitor) Err(FatError::NoBatteryMonitor)
} }
fn cycle_count(&mut self) -> Result<u16, BatteryError> { async fn cycle_count(&mut self) -> FatResult<u16> {
Err(BatteryError::NoBatteryMonitor) Err(FatError::NoBatteryMonitor)
} }
fn state_health_percent(&mut self) -> Result<u16, BatteryError> { async fn state_health_percent(&mut self) -> FatResult<u16> {
Err(BatteryError::NoBatteryMonitor) Err(FatError::NoBatteryMonitor)
} }
fn bat_temperature(&mut self) -> Result<u16, BatteryError> { async fn bat_temperature(&mut self) -> FatResult<u16> {
Err(BatteryError::NoBatteryMonitor) Err(FatError::NoBatteryMonitor)
} }
fn get_battery_state(&mut self) -> Result<BatteryState, BatteryError> { async fn get_battery_state(&mut self) -> FatResult<BatteryState> {
Ok(BatteryState::Unknown) Ok(BatteryState::Unknown)
} }
} }
@@ -100,115 +99,167 @@ impl BatteryInteraction for NoBatteryMonitor {
#[allow(dead_code)] #[allow(dead_code)]
pub struct WchI2cSlave {} pub struct WchI2cSlave {}
pub struct BQ34Z100G1<'a> { pub type I2cDev = I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>;
pub battery_driver: Bq34z100g1Driver<MutexDevice<'a, I2cDriver<'a>>, Delay>,
pub struct BQ34Z100G1 {
pub battery_driver: Bq34z100g1Driver<I2cDev, Delay>,
} }
impl BatteryInteraction for BQ34Z100G1<'_> { #[async_trait]
fn state_charge_percent(&mut self) -> Result<f32, BatteryError> { impl BatteryInteraction for BQ34Z100G1 {
Ok(self.battery_driver.state_of_charge().map(f32::from)?) async fn state_charge_percent(&mut self) -> FatResult<f32> {
self.battery_driver
.state_of_charge()
.map(|v| v as f32)
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
} }
fn remaining_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> { async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16> {
Ok(self.battery_driver.remaining_capacity()?) self.battery_driver
.remaining_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
} }
fn max_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> { async fn max_milli_ampere_hour(&mut self) -> FatResult<u16> {
Ok(self.battery_driver.full_charge_capacity()?) self.battery_driver
.full_charge_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
} }
fn design_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> { async fn design_milli_ampere_hour(&mut self) -> FatResult<u16> {
Ok(self.battery_driver.design_capacity()?) self.battery_driver
.design_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
} }
fn voltage_milli_volt(&mut self) -> Result<u16, BatteryError> { async fn voltage_milli_volt(&mut self) -> FatResult<u16> {
Ok(self.battery_driver.voltage()?) self.battery_driver.voltage().map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
} }
fn average_current_milli_ampere(&mut self) -> Result<i16, BatteryError> { async fn average_current_milli_ampere(&mut self) -> FatResult<i16> {
Ok(self.battery_driver.average_current()?) self.battery_driver
.average_current()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
} }
fn cycle_count(&mut self) -> Result<u16, BatteryError> { async fn cycle_count(&mut self) -> FatResult<u16> {
Ok(self.battery_driver.cycle_count()?) self.battery_driver
.cycle_count()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
} }
fn state_health_percent(&mut self) -> Result<u16, BatteryError> { async fn state_health_percent(&mut self) -> FatResult<u16> {
Ok(self.battery_driver.state_of_health()?) self.battery_driver
.state_of_health()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
} }
fn bat_temperature(&mut self) -> Result<u16, BatteryError> { async fn bat_temperature(&mut self) -> FatResult<u16> {
Ok(self.battery_driver.temperature()?) self.battery_driver
.temperature()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
} }
fn get_battery_state(&mut self) -> Result<BatteryState, BatteryError> { async fn get_battery_state(&mut self) -> FatResult<BatteryState> {
Ok(BatteryState::Info(BatteryInfo { Ok(BatteryState::Info(BatteryInfo {
voltage_milli_volt: self.voltage_milli_volt()?, voltage_milli_volt: self.voltage_milli_volt().await?,
average_current_milli_ampere: self.average_current_milli_ampere()?, average_current_milli_ampere: self.average_current_milli_ampere().await?,
cycle_count: self.cycle_count()?, cycle_count: self.cycle_count().await?,
design_milli_ampere_hour: self.design_milli_ampere_hour()?, design_milli_ampere_hour: self.design_milli_ampere_hour().await?,
remaining_milli_ampere_hour: self.remaining_milli_ampere_hour()?, remaining_milli_ampere_hour: self.remaining_milli_ampere_hour().await?,
state_of_charge: self.state_charge_percent()?, state_of_charge: self.state_charge_percent().await?,
state_of_health: self.state_health_percent()?, state_of_health: self.state_health_percent().await?,
temperature: self.bat_temperature()?, temperature: self.bat_temperature().await?,
})) }))
} }
} }
pub fn print_battery_bq34z100( pub fn print_battery_bq34z100(
battery_driver: &mut Bq34z100g1Driver<MutexDevice<I2cDriver<'_>>, Delay>, battery_driver: &mut Bq34z100g1Driver<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>, Delay>,
) -> anyhow::Result<(), Bq34Z100Error<I2cError>> { ) -> FatResult<()> {
println!("Try communicating with battery"); log::info!("Try communicating with battery");
let fwversion = battery_driver.fw_version().unwrap_or_else(|e| { let fwversion = battery_driver.fw_version().unwrap_or_else(|e| {
println!("Firmware {:?}", e); log::info!("Firmware {:?}", e);
0 0
}); });
println!("fw version is {}", fwversion); log::info!("fw version is {}", fwversion);
let design_capacity = battery_driver.design_capacity().unwrap_or_else(|e| { let design_capacity = battery_driver.design_capacity().unwrap_or_else(|e| {
println!("Design capacity {:?}", e); log::info!("Design capacity {:?}", e);
0 0
}); });
println!("Design Capacity {}", design_capacity); log::info!("Design Capacity {}", design_capacity);
if design_capacity == 1000 { if design_capacity == 1000 {
println!("Still stock configuring battery, readouts are likely to be wrong!"); log::info!("Still stock configuring battery, readouts are likely to be wrong!");
} }
let flags = battery_driver.get_flags_decoded()?; let flags = battery_driver.get_flags_decoded().unwrap_or(Flags {
println!("Flags {:?}", flags); fast_charge_allowed: false,
full_chage: false,
charging_not_allowed: false,
charge_inhibit: false,
bat_low: false,
bat_high: false,
over_temp_discharge: false,
over_temp_charge: false,
discharge: false,
state_of_charge_f: false,
state_of_charge_1: false,
cf: false,
ocv_taken: false,
});
log::info!("Flags {:?}", flags);
let chem_id = battery_driver.chem_id().unwrap_or_else(|e| { let chem_id = battery_driver.chem_id().unwrap_or_else(|e| {
println!("Chemid {:?}", e); log::info!("Chemid {:?}", e);
0 0
}); });
let bat_temp = battery_driver.internal_temperature().unwrap_or_else(|e| { let bat_temp = battery_driver.internal_temperature().unwrap_or_else(|e| {
println!("Bat Temp {:?}", e); log::info!("Bat Temp {:?}", e);
0 0
}); });
let temp_c = Temperature::from_kelvin(bat_temp as f64 / 10_f64).as_celsius(); let temp_c = Temperature::from_kelvin(bat_temp as f64 / 10_f64).as_celsius();
let voltage = battery_driver.voltage().unwrap_or_else(|e| { let voltage = battery_driver.voltage().unwrap_or_else(|e| {
println!("Bat volt {:?}", e); log::info!("Bat volt {:?}", e);
0 0
}); });
let current = battery_driver.current().unwrap_or_else(|e| { let current = battery_driver.current().unwrap_or_else(|e| {
println!("Bat current {:?}", e); log::info!("Bat current {:?}", e);
0 0
}); });
let state = battery_driver.state_of_charge().unwrap_or_else(|e| { let state = battery_driver.state_of_charge().unwrap_or_else(|e| {
println!("Bat Soc {:?}", e); log::info!("Bat Soc {:?}", e);
0 0
}); });
let charge_voltage = battery_driver.charge_voltage().unwrap_or_else(|e| { let charge_voltage = battery_driver.charge_voltage().unwrap_or_else(|e| {
println!("Bat Charge Volt {:?}", e); log::info!("Bat Charge Volt {:?}", e);
0 0
}); });
let charge_current = battery_driver.charge_current().unwrap_or_else(|e| { let charge_current = battery_driver.charge_current().unwrap_or_else(|e| {
println!("Bat Charge Current {:?}", e); log::info!("Bat Charge Current {:?}", e);
0 0
}); });
println!("ChemId: {} Current voltage {} and current {} with charge {}% and temp {} CVolt: {} CCur {}", chem_id, voltage, current, state, temp_c, charge_voltage, charge_current); log::info!("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.unsealed();
let _ = battery_driver.it_enable(); let _ = battery_driver.it_enable();
anyhow::Result::Ok(()) Ok(())
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,64 +1,63 @@
use crate::alloc::boxed::Box;
use crate::fat_error::{FatError, FatResult};
use crate::hal::esp::Esp; use crate::hal::esp::Esp;
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction}; use crate::hal::rtc::{BackupHeader, RTCModuleInteraction};
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::hal::{deep_sleep, BoardInteraction, FreePeripherals, Sensor}; use crate::hal::{BoardInteraction, FreePeripherals, Sensor, TIME_ACCESS};
use crate::{ use crate::{
bail,
config::PlantControllerConfig, config::PlantControllerConfig,
hal::battery::{BatteryInteraction, NoBatteryMonitor}, hal::battery::{BatteryInteraction, NoBatteryMonitor},
}; };
use anyhow::{bail, Result}; use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use embedded_hal::digital::OutputPin; use esp_hal::gpio::{Level, Output, OutputConfig};
use esp_idf_hal::gpio::{IOPin, Pull};
use esp_idf_hal::gpio::{InputOutput, PinDriver};
use measurements::{Current, Voltage}; use measurements::{Current, Voltage};
pub struct Initial<'a> { pub struct Initial<'a> {
pub(crate) general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, pub(crate) general_fault: Output<'a>,
pub(crate) esp: Esp<'a>, pub(crate) esp: Esp<'a>,
pub(crate) config: PlantControllerConfig, pub(crate) config: PlantControllerConfig,
pub(crate) battery: Box<dyn BatteryInteraction + Send>, pub(crate) battery: Box<dyn BatteryInteraction + Send>,
pub rtc: Box<dyn RTCModuleInteraction + Send>, pub rtc: Box<dyn RTCModuleInteraction + Send>,
} }
struct NoRTC {} pub(crate) struct NoRTC {}
#[async_trait]
impl RTCModuleInteraction for NoRTC { impl RTCModuleInteraction for NoRTC {
fn get_backup_info(&mut self) -> Result<BackupHeader> { async fn get_backup_info(&mut self) -> Result<BackupHeader, FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn get_backup_config(&mut self) -> Result<Vec<u8>> { async fn get_backup_config(&mut self, _chunk: usize) -> FatResult<([u8; 32], usize, u16)> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn backup_config(&mut self, _bytes: &[u8]) -> Result<()> { async fn backup_config(&mut self, _offset: usize, _bytes: &[u8]) -> FatResult<()> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn get_rtc_time(&mut self) -> Result<DateTime<Utc>> { async fn backup_config_finalize(&mut self, _crc: u16, _length: usize) -> FatResult<()> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn set_rtc_time(&mut self, _time: &DateTime<Utc>) -> Result<()> { async fn get_rtc_time(&mut self) -> Result<DateTime<Utc>, FatError> {
bail!("Please configure board revision")
}
async fn set_rtc_time(&mut self, _time: &DateTime<Utc>) -> Result<(), FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
} }
pub(crate) fn create_initial_board( pub(crate) fn create_initial_board(
free_pins: FreePeripherals, free_pins: FreePeripherals<'static>,
fs_mount_error: bool,
config: PlantControllerConfig, config: PlantControllerConfig,
esp: Esp<'static>, esp: Esp<'static>,
) -> Result<Box<dyn BoardInteraction<'static> + Send>> { ) -> Result<Box<dyn BoardInteraction<'static> + Send>, FatError> {
println!("Start initial"); log::info!("Start initial");
let mut general_fault = PinDriver::input_output(free_pins.gpio6.downgrade())?; let general_fault = Output::new(free_pins.gpio23, Level::Low, OutputConfig::default());
general_fault.set_pull(Pull::Floating)?;
general_fault.set_low()?;
if fs_mount_error {
general_fault.set_high()?
}
let v = Initial { let v = Initial {
general_fault, general_fault,
config, config,
@@ -69,9 +68,10 @@ pub(crate) fn create_initial_board(
Ok(Box::new(v)) Ok(Box::new(v))
} }
#[async_trait]
impl<'a> BoardInteraction<'a> for Initial<'a> { impl<'a> BoardInteraction<'a> for Initial<'a> {
fn get_tank_sensor(&mut self) -> Option<&mut TankSensor<'a>> { fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
None bail!("Please configure board revision")
} }
fn get_esp(&mut self) -> &mut Esp<'a> { fn get_esp(&mut self) -> &mut Esp<'a> {
@@ -90,55 +90,58 @@ impl<'a> BoardInteraction<'a> for Initial<'a> {
&mut self.rtc &mut self.rtc
} }
fn set_charge_indicator(&mut self, _charging: bool) -> Result<()> { async fn set_charge_indicator(&mut self, _charging: bool) -> Result<(), FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn deep_sleep(&mut self, duration_in_ms: u64) -> ! { async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
deep_sleep(duration_in_ms) let rtc = TIME_ACCESS.get().await.lock().await;
self.esp.deep_sleep(duration_in_ms, rtc);
} }
fn is_day(&self) -> bool { fn is_day(&self) -> bool {
false false
} }
fn light(&mut self, _enable: bool) -> Result<()> { async fn light(&mut self, _enable: bool) -> Result<(), FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn pump(&mut self, _plant: usize, _enable: bool) -> Result<()> { async fn pump(&mut self, _plant: usize, _enable: bool) -> Result<(), FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn pump_current(&mut self, _plant: usize) -> Result<Current> { async fn pump_current(&mut self, _plant: usize) -> Result<Current, FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn fault(&mut self, _plant: usize, _enable: bool) -> Result<()> { async fn fault(&mut self, _plant: usize, _enable: bool) -> Result<(), FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn measure_moisture_hz(&mut self, _plant: usize, _sensor: Sensor) -> Result<f32> { async fn measure_moisture_hz(
&mut self,
_plant: usize,
_sensor: Sensor,
) -> Result<f32, FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn general_fault(&mut self, enable: bool) { async fn general_fault(&mut self, enable: bool) {
let _ = self.general_fault.set_state(enable.into()); self.general_fault.set_level(enable.into());
} }
fn test(&mut self) -> Result<()> { async fn test(&mut self) -> Result<(), FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn set_config(&mut self, config: PlantControllerConfig) -> anyhow::Result<()> { fn set_config(&mut self, config: PlantControllerConfig) {
self.config = config; self.config = config;
self.esp.save_config(&self.config)?;
anyhow::Ok(())
} }
fn get_mptt_voltage(&mut self) -> Result<Voltage> { async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
fn get_mptt_current(&mut self) -> Result<Current> { async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
} }

View File

@@ -0,0 +1,63 @@
use embedded_storage::{ReadStorage, Storage};
use esp_bootloader_esp_idf::partitions::FlashRegion;
use esp_storage::FlashStorage;
use littlefs2::consts::U512 as lfsCache;
use littlefs2::consts::U512 as lfsLookahead;
use littlefs2::driver::Storage as lfs2Storage;
use littlefs2::io::Error as lfs2Error;
use littlefs2::io::Result as lfs2Result;
use log::error;
pub struct LittleFs2Filesystem {
pub(crate) storage: &'static mut FlashRegion<'static, FlashStorage>,
}
impl lfs2Storage for LittleFs2Filesystem {
const READ_SIZE: usize = 256;
const WRITE_SIZE: usize = 512;
const BLOCK_SIZE: usize = 512; //usually optimal for flash access
const BLOCK_COUNT: usize = 8 * 1024 * 1024 / 512; //8mb in 32kb blocks
const BLOCK_CYCLES: isize = 100;
type CACHE_SIZE = lfsCache;
type LOOKAHEAD_SIZE = lfsLookahead;
fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> {
let read_size: usize = Self::READ_SIZE;
assert_eq!(off % read_size, 0);
assert_eq!(buf.len() % read_size, 0);
match self.storage.read(off as u32, buf) {
Ok(..) => Ok(buf.len()),
Err(err) => {
error!("Littlefs2Filesystem read error: {:?}", err);
Err(lfs2Error::IO)
}
}
}
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
let write_size: usize = Self::WRITE_SIZE;
assert_eq!(off % write_size, 0);
assert_eq!(data.len() % write_size, 0);
match self.storage.write(off as u32, data) {
Ok(..) => Ok(data.len()),
Err(err) => {
error!("Littlefs2Filesystem write error: {:?}", err);
Err(lfs2Error::IO)
}
}
}
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
let block_size: usize = Self::BLOCK_SIZE;
debug_assert!(off % block_size == 0);
debug_assert!(len % block_size == 0);
//match self.storage.erase(off as u32, len as u32) {
//anyhow::Result::Ok(..) => lfs2Result::Ok(len),
//Err(err) => {
//error!("Littlefs2Filesystem erase error: {:?}", err);
//Err(lfs2Error::IO)
// }
//}
lfs2Result::Ok(len)
}
}

View File

@@ -1,80 +1,118 @@
pub(crate) mod battery; pub(crate) mod battery;
mod esp; pub mod esp;
mod initial_hal; mod initial_hal;
mod rtc; mod little_fs2storage_adapter;
pub(crate) mod rtc;
mod v3_hal; mod v3_hal;
mod v3_shift_register;
mod v4_hal; mod v4_hal;
mod water;
mod v4_sensor; mod v4_sensor;
mod water;
use crate::alloc::string::ToString;
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction}; use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
use crate::hal::water::TankSensor; use esp_hal::peripherals::Peripherals;
use esp_hal::peripherals::ADC1;
use esp_hal::peripherals::APB_SARADC;
use esp_hal::peripherals::GPIO0;
use esp_hal::peripherals::GPIO10;
use esp_hal::peripherals::GPIO11;
use esp_hal::peripherals::GPIO12;
use esp_hal::peripherals::GPIO13;
use esp_hal::peripherals::GPIO14;
use esp_hal::peripherals::GPIO15;
use esp_hal::peripherals::GPIO16;
use esp_hal::peripherals::GPIO17;
use esp_hal::peripherals::GPIO18;
use esp_hal::peripherals::GPIO2;
use esp_hal::peripherals::GPIO21;
use esp_hal::peripherals::GPIO22;
use esp_hal::peripherals::GPIO23;
use esp_hal::peripherals::GPIO24;
use esp_hal::peripherals::GPIO25;
use esp_hal::peripherals::GPIO26;
use esp_hal::peripherals::GPIO27;
use esp_hal::peripherals::GPIO28;
use esp_hal::peripherals::GPIO29;
use esp_hal::peripherals::GPIO3;
use esp_hal::peripherals::GPIO30;
use esp_hal::peripherals::GPIO4;
use esp_hal::peripherals::GPIO5;
use esp_hal::peripherals::GPIO6;
use esp_hal::peripherals::GPIO7;
use esp_hal::peripherals::GPIO8;
use esp_hal::peripherals::PCNT;
use esp_hal::peripherals::TWAI0;
use crate::{ use crate::{
bail,
config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig}, config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig},
hal::{ hal::{
battery::{print_battery_bq34z100, BatteryInteraction, NoBatteryMonitor}, battery::{BatteryInteraction, NoBatteryMonitor},
esp::Esp, esp::Esp,
}, },
log::{log, LogMessage}, log::LogMessage,
BOARD_ACCESS,
}; };
use anyhow::{Ok, Result}; use alloc::boxed::Box;
use battery::BQ34Z100G1; use alloc::format;
use alloc::sync::Arc;
use async_trait::async_trait;
use bq34z100::Bq34z100g1Driver; use bq34z100::Bq34z100g1Driver;
use chrono::{DateTime, FixedOffset, Utc};
use core::cell::RefCell;
use ds323x::ic::DS3231;
use ds323x::interface::I2cInterface;
use ds323x::{DateTimeAccess, Ds323x}; use ds323x::{DateTimeAccess, Ds323x};
use eeprom24x::addr_size::TwoBytes;
use eeprom24x::page_size::B32;
use eeprom24x::unique_serial::No;
use eeprom24x::{Eeprom24x, SlaveAddr, Storage}; use eeprom24x::{Eeprom24x, SlaveAddr, Storage};
use embedded_hal_bus::i2c::MutexDevice; use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use esp_idf_hal::pcnt::PCNT1; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use esp_idf_hal::{ use embassy_sync::blocking_mutex::CriticalSectionMutex;
adc::ADC1, use esp_bootloader_esp_idf::partitions::{
delay::Delay, AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry,
gpio::{
Gpio0, Gpio1, Gpio10, Gpio11, Gpio12, Gpio13, Gpio14, Gpio15, Gpio16, Gpio17, Gpio18,
Gpio2, Gpio21, Gpio22, Gpio23, Gpio24, Gpio25, Gpio26, Gpio27, Gpio28, Gpio29, Gpio3,
Gpio30, Gpio4, Gpio5, Gpio6, Gpio7, Gpio8, IOPin, PinDriver, Pull,
},
i2c::{APBTickType, I2cConfig, I2cDriver},
pcnt::PCNT0,
prelude::Peripherals,
reset::ResetReason,
units::FromValueType,
}; };
use esp_idf_svc::{eventloop::EspSystemEventLoop, nvs::EspDefaultNvsPartition, wifi::EspWifi}; use esp_hal::clock::CpuClock;
use esp_idf_sys::{ use esp_hal::gpio::{Input, InputConfig, Pull};
esp_deep_sleep, esp_restart, esp_sleep_enable_ext1_wakeup,
esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW,
};
use esp_ota::mark_app_valid;
use measurements::{Current, Voltage}; use measurements::{Current, Voltage};
use once_cell::sync::Lazy;
use std::result::Result::Ok as OkStd; use crate::fat_error::{FatError, FatResult};
use std::sync::Mutex; use crate::hal::battery::{print_battery_bq34z100, BQ34Z100G1};
use std::time::Duration; use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use esp_idf_hal::can::CAN; use crate::hal::water::TankSensor;
use crate::log::LOG_ACCESS;
use embassy_sync::mutex::Mutex;
use embassy_sync::once_lock::OnceLock;
use esp_alloc as _;
use esp_backtrace as _;
use esp_bootloader_esp_idf::ota::Slot;
use esp_hal::delay::Delay;
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
use esp_hal::pcnt::unit::Unit;
use esp_hal::pcnt::Pcnt;
use esp_hal::rng::Rng;
use esp_hal::rtc_cntl::{Rtc, SocResetReason};
use esp_hal::system::reset_reason;
use esp_hal::time::Rate;
use esp_hal::timer::timg::TimerGroup;
use esp_hal::Blocking;
use esp_storage::FlashStorage;
use esp_wifi::{init, EspWifiController};
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
use littlefs2::object_safe::DynStorage;
use log::{info, warn};
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
//Only support for 8 right now! //Only support for 8 right now!
pub const PLANT_COUNT: usize = 8; pub const PLANT_COUNT: usize = 8;
const TANK_MULTI_SAMPLE: usize = 11; const TANK_MULTI_SAMPLE: usize = 11;
pub static I2C_DRIVER: OnceLock<
pub static I2C_DRIVER: Lazy<Mutex<I2cDriver<'static>>> = Lazy::new(PlantHal::create_i2c); embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
> = OnceLock::new();
fn deep_sleep(duration_in_ms: u64) -> ! {
unsafe {
//if we don't do this here, we might just revert newly flashed firmware
mark_app_valid();
//allow early wakeup by pressing the boot button
if duration_in_ms == 0 {
esp_restart();
} else {
//configure gpio 1 to wakeup on low, reused boot button for this
esp_sleep_enable_ext1_wakeup(
0b10u64,
esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW,
);
esp_deep_sleep(duration_in_ms);
}
};
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Sensor { pub enum Sensor {
@@ -88,205 +126,370 @@ pub struct HAL<'a> {
pub board_hal: Box<dyn BoardInteraction<'a> + Send>, pub board_hal: Box<dyn BoardInteraction<'a> + Send>,
} }
#[async_trait]
pub trait BoardInteraction<'a> { pub trait BoardInteraction<'a> {
fn get_tank_sensor(&mut self) -> Option<&mut TankSensor<'a>>; fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>;
fn get_esp(&mut self) -> &mut Esp<'a>; fn get_esp(&mut self) -> &mut Esp<'a>;
fn get_config(&mut self) -> &PlantControllerConfig; fn get_config(&mut self) -> &PlantControllerConfig;
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send>; fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send>;
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send>; fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send>;
fn set_charge_indicator(&mut self, charging: bool) -> Result<()>; async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError>;
fn deep_sleep(&mut self, duration_in_ms: u64) -> !; async fn deep_sleep(&mut self, duration_in_ms: u64) -> !;
fn is_day(&self) -> bool; fn is_day(&self) -> bool;
//should be multsampled //should be multsampled
fn light(&mut self, enable: bool) -> Result<()>; async fn light(&mut self, enable: bool) -> Result<(), FatError>;
fn pump(&mut self, plant: usize, enable: bool) -> Result<()>; async fn pump(&mut self, plant: usize, enable: bool) -> Result<(), FatError>;
fn pump_current(&mut self, plant: usize) -> Result<Current>; async fn pump_current(&mut self, plant: usize) -> Result<Current, FatError>;
fn fault(&mut self, plant: usize, enable: bool) -> Result<()>; async fn fault(&mut self, plant: usize, enable: bool) -> Result<(), FatError>;
fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32>; async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError>;
fn general_fault(&mut self, enable: bool); async fn general_fault(&mut self, enable: bool);
fn test(&mut self) -> Result<()>; async fn test(&mut self) -> Result<(), FatError>;
fn set_config(&mut self, config: PlantControllerConfig) -> Result<()>; fn set_config(&mut self, config: PlantControllerConfig);
fn get_mptt_voltage(&mut self) -> anyhow::Result<Voltage>; async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError>;
fn get_mptt_current(&mut self) -> anyhow::Result<Current>; async fn get_mptt_current(&mut self) -> Result<Current, FatError>;
async fn progress(&mut self, counter: u32) {
let current = counter % PLANT_COUNT as u32;
for led in 0..PLANT_COUNT {
if let Err(err) = self.fault(led, current == led as u32).await {
warn!("Fault on plant {}: {:?}", led, err);
}
}
let even = counter % 2 == 0;
let _ = self.general_fault(even.into()).await;
} }
impl dyn BoardInteraction<'_> { async fn clear_progress(&mut self) {
//the counter is just some arbitrary number that increases whenever some progress was made, try to keep the updates < 10 per second for ux reasons
fn _progress(&mut self, counter: u32) {
let even = counter % 2 == 0;
let current = counter / (PLANT_COUNT as u32);
for led in 0..PLANT_COUNT { for led in 0..PLANT_COUNT {
self.fault(led, current == led as u32).unwrap(); if let Err(err) = self.fault(led, false).await {
warn!("Fault on plant {}: {:?}", led, err);
} }
let _ = self.general_fault(even.into()); }
let _ = self.general_fault(false).await;
} }
} }
#[allow(dead_code)] #[allow(dead_code)]
pub struct FreePeripherals { pub struct FreePeripherals<'a> {
pub gpio0: Gpio0, pub gpio0: GPIO0<'a>,
pub gpio1: Gpio1, pub gpio2: GPIO2<'a>,
pub gpio2: Gpio2, pub gpio3: GPIO3<'a>,
pub gpio3: Gpio3, pub gpio4: GPIO4<'a>,
pub gpio4: Gpio4, pub gpio5: GPIO5<'a>,
pub gpio5: Gpio5, pub gpio6: GPIO6<'a>,
pub gpio6: Gpio6, pub gpio7: GPIO7<'a>,
pub gpio7: Gpio7, pub gpio8: GPIO8<'a>,
pub gpio8: Gpio8, // //config button here
//config button here pub gpio10: GPIO10<'a>,
pub gpio10: Gpio10, pub gpio11: GPIO11<'a>,
pub gpio11: Gpio11, pub gpio12: GPIO12<'a>,
pub gpio12: Gpio12, pub gpio13: GPIO13<'a>,
pub gpio13: Gpio13, pub gpio14: GPIO14<'a>,
pub gpio14: Gpio14, pub gpio15: GPIO15<'a>,
pub gpio15: Gpio15, pub gpio16: GPIO16<'a>,
pub gpio16: Gpio16, pub gpio17: GPIO17<'a>,
pub gpio17: Gpio17, pub gpio18: GPIO18<'a>,
pub gpio18: Gpio18, // //i2c here
//i2c here pub gpio21: GPIO21<'a>,
pub gpio21: Gpio21, pub gpio22: GPIO22<'a>,
pub gpio22: Gpio22, pub gpio23: GPIO23<'a>,
pub gpio23: Gpio23, pub gpio24: GPIO24<'a>,
pub gpio24: Gpio24, pub gpio25: GPIO25<'a>,
pub gpio25: Gpio25, pub gpio26: GPIO26<'a>,
pub gpio26: Gpio26, pub gpio27: GPIO27<'a>,
pub gpio27: Gpio27, pub gpio28: GPIO28<'a>,
pub gpio28: Gpio28, pub gpio29: GPIO29<'a>,
pub gpio29: Gpio29, pub gpio30: GPIO30<'a>,
pub gpio30: Gpio30, pub twai: TWAI0<'a>,
pub pcnt0: PCNT0, pub pcnt0: Unit<'a, 0>,
pub pcnt1: PCNT1, pub pcnt1: Unit<'a, 1>,
pub adc1: ADC1, pub adc1: ADC1<'a>,
pub can: CAN, }
macro_rules! mk_static {
($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val));
x
}};
} }
impl PlantHal { impl PlantHal {
fn create_i2c() -> Mutex<I2cDriver<'static>> { pub async fn create() -> Result<Mutex<CriticalSectionRawMutex, HAL<'static>>, FatError> {
let peripherals = unsafe { Peripherals::new() }; let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals: Peripherals = esp_hal::init(config);
let config = I2cConfig::new() esp_alloc::heap_allocator!(size: 64 * 1024);
.scl_enable_pullup(true) esp_alloc::heap_allocator!(#[link_section = ".dram2_uninit"] size: 64000);
.sda_enable_pullup(true)
.baudrate(100_u32.kHz().into())
.timeout(APBTickType::from(Duration::from_millis(100)));
let i2c = peripherals.i2c0; let rtc: Rtc = Rtc::new(peripherals.LPWR);
let scl = peripherals.pins.gpio19.downgrade(); TIME_ACCESS
let sda = peripherals.pins.gpio20.downgrade(); .init(Mutex::new(rtc))
.map_err(|_| FatError::String {
error: "Init error rct".to_string(),
})?;
Mutex::new(I2cDriver::new(i2c, sda, scl, &config).unwrap()) let systimer = SystemTimer::new(peripherals.SYSTIMER);
}
pub fn create() -> Result<Mutex<HAL<'static>>> { let boot_button = Input::new(
let peripherals = Peripherals::take()?; peripherals.GPIO9,
let sys_loop = EspSystemEventLoop::take()?; InputConfig::default().with_pull(Pull::None),
let nvs = EspDefaultNvsPartition::take()?; );
let wifi_driver = EspWifi::new(peripherals.modem, sys_loop, Some(nvs))?;
let mut boot_button = PinDriver::input(peripherals.pins.gpio9.downgrade())?; // Reserve GPIO1 for deep sleep wake (configured just before entering sleep)
boot_button.set_pull(Pull::Floating)?; let wake_gpio1 = peripherals.GPIO1;
let rng = Rng::new(peripherals.RNG);
let timg0 = TimerGroup::new(peripherals.TIMG0);
let esp_wifi_ctrl = &*mk_static!(
EspWifiController<'static>,
init(timg0.timer0, rng.clone()).expect("Could not init wifi controller")
);
let (controller, interfaces) =
esp_wifi::wifi::new(&esp_wifi_ctrl, peripherals.WIFI).expect("Could not init wifi");
use esp_hal::timer::systimer::SystemTimer;
esp_hal_embassy::init(systimer.alarm0);
//let mut adc1 = Adc::new(peripherals.ADC1, adc1_config);
//
let pcnt_module = Pcnt::new(peripherals.PCNT);
let free_pins = FreePeripherals { let free_pins = FreePeripherals {
can: peripherals.can, // can: peripherals.can,
adc1: peripherals.adc1, // adc1: peripherals.adc1,
pcnt0: peripherals.pcnt0, // pcnt0: peripherals.pcnt0,
pcnt1: peripherals.pcnt1, // pcnt1: peripherals.pcnt1,
gpio0: peripherals.pins.gpio0, gpio0: peripherals.GPIO0,
gpio1: peripherals.pins.gpio1, gpio2: peripherals.GPIO2,
gpio2: peripherals.pins.gpio2, gpio3: peripherals.GPIO3,
gpio3: peripherals.pins.gpio3, gpio4: peripherals.GPIO4,
gpio4: peripherals.pins.gpio4, gpio5: peripherals.GPIO5,
gpio5: peripherals.pins.gpio5, gpio6: peripherals.GPIO6,
gpio6: peripherals.pins.gpio6, gpio7: peripherals.GPIO7,
gpio7: peripherals.pins.gpio7, gpio8: peripherals.GPIO8,
gpio8: peripherals.pins.gpio8, gpio10: peripherals.GPIO10,
gpio10: peripherals.pins.gpio10, gpio11: peripherals.GPIO11,
gpio11: peripherals.pins.gpio11, gpio12: peripherals.GPIO12,
gpio12: peripherals.pins.gpio12, gpio13: peripherals.GPIO13,
gpio13: peripherals.pins.gpio13, gpio14: peripherals.GPIO14,
gpio14: peripherals.pins.gpio14, gpio15: peripherals.GPIO15,
gpio15: peripherals.pins.gpio15, gpio16: peripherals.GPIO16,
gpio16: peripherals.pins.gpio16, gpio17: peripherals.GPIO17,
gpio17: peripherals.pins.gpio17, gpio18: peripherals.GPIO18,
gpio18: peripherals.pins.gpio18, gpio21: peripherals.GPIO21,
gpio21: peripherals.pins.gpio21, gpio22: peripherals.GPIO22,
gpio22: peripherals.pins.gpio22, gpio23: peripherals.GPIO23,
gpio23: peripherals.pins.gpio23, gpio24: peripherals.GPIO24,
gpio24: peripherals.pins.gpio24, gpio25: peripherals.GPIO25,
gpio25: peripherals.pins.gpio25, gpio26: peripherals.GPIO26,
gpio26: peripherals.pins.gpio26, gpio27: peripherals.GPIO27,
gpio27: peripherals.pins.gpio27, gpio28: peripherals.GPIO28,
gpio28: peripherals.pins.gpio28, gpio29: peripherals.GPIO29,
gpio29: peripherals.pins.gpio29, gpio30: peripherals.GPIO30,
gpio30: peripherals.pins.gpio30, twai: peripherals.TWAI0,
pcnt0: pcnt_module.unit0,
pcnt1: pcnt_module.unit1,
adc1: peripherals.ADC1,
}; };
let tablebuffer = mk_static!(
[u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN],
[0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]
);
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
let pt =
esp_bootloader_esp_idf::partitions::read_partition_table(storage_ota, tablebuffer)?;
// List all partitions - this is just FYI
for i in 0..pt.len() {
info!("{:?}", pt.get_partition(i));
}
let ota_data = mk_static!(
PartitionEntry,
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
DataPartitionSubType::Ota,
))?
.expect("No OTA data partition found")
);
let ota_data = mk_static!(
FlashRegion<FlashStorage>,
ota_data.as_embedded_storage(storage_ota)
);
let mut ota = esp_bootloader_esp_idf::ota::Ota::new(ota_data)?;
let ota_partition = match ota.current_slot()? {
Slot::None => {
panic!("No OTA slot active?");
}
Slot::Slot0 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota0,
))?
.expect("No OTA slot0 found"),
Slot::Slot1 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota1,
))?
.expect("No OTA slot1 found"),
};
let ota_next = mk_static!(PartitionEntry, ota_partition);
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
let ota_next = mk_static!(
FlashRegion<FlashStorage>,
ota_next.as_embedded_storage(storage_ota)
);
let data_partition = pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
DataPartitionSubType::LittleFs,
))?
.expect("Data partition with littlefs not found");
let data_partition = mk_static!(PartitionEntry, data_partition);
let storage_data = mk_static!(FlashStorage, FlashStorage::new());
let data = mk_static!(
FlashRegion<FlashStorage>,
data_partition.as_embedded_storage(storage_data)
);
let lfs2filesystem = mk_static!(LittleFs2Filesystem, LittleFs2Filesystem { storage: data });
let alloc = mk_static!(Allocation<LittleFs2Filesystem>, lfs2Filesystem::allocate());
if lfs2filesystem.is_mountable() {
log::info!("Littlefs2 filesystem is mountable");
} else {
match lfs2filesystem.format() {
Result::Ok(..) => {
log::info!("Littlefs2 filesystem is formatted");
}
Err(err) => {
bail!("Littlefs2 filesystem could not be formatted: {:?}", err);
}
}
}
let fs = Arc::new(Mutex::new(
lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"),
));
let ap = interfaces.ap;
let sta = interfaces.sta;
let mut esp = Esp { let mut esp = Esp {
mqtt_client: None, fs,
wifi_driver, rng,
controller: Arc::new(Mutex::new(controller)),
interface_sta: Some(sta),
interface_ap: Some(ap),
boot_button, boot_button,
delay: Delay::new(1000), wake_gpio1,
ota,
ota_next,
}; };
//init,reset rtc memory depending on cause //init,reset rtc memory depending on cause
let mut init_rtc_store: bool = false; let mut init_rtc_store: bool = false;
let mut to_config_mode: bool = false; let mut to_config_mode: bool = false;
let reasons = ResetReason::get(); let reasons = match reset_reason() {
match reasons { None => "unknown",
ResetReason::Software => {} Some(reason) => match reason {
ResetReason::ExternalPin => {} SocResetReason::ChipPowerOn => "power on",
ResetReason::Watchdog => { SocResetReason::CoreSDIO => "sdio reset",
SocResetReason::CoreMwdt0 => "Watchdog Main",
SocResetReason::CoreMwdt1 => "Watchdog 1",
SocResetReason::CoreRtcWdt => "Watchdog RTC",
SocResetReason::Cpu0Mwdt0 => "Watchdog MCpu0",
SocResetReason::Cpu0Sw => "software reset cpu0",
SocResetReason::SysRtcWdt => "Watchdog Sys rtc",
SocResetReason::Cpu0Mwdt1 => "cpu0 mwdt1",
SocResetReason::SysSuperWdt => "Watchdog Super",
SocResetReason::Cpu0RtcWdt => {
init_rtc_store = true; init_rtc_store = true;
"Watchdog RTC cpu0"
} }
ResetReason::Sdio => init_rtc_store = true, SocResetReason::CoreSw => "software reset",
ResetReason::Panic => init_rtc_store = true, SocResetReason::CoreDeepSleep => "deep sleep",
ResetReason::InterruptWatchdog => init_rtc_store = true, SocResetReason::SysBrownOut => "sys brown out",
ResetReason::PowerOn => init_rtc_store = true, SocResetReason::CoreEfuseCrc => "core efuse crc",
ResetReason::Unknown => init_rtc_store = true, SocResetReason::CoreUsbUart => {
ResetReason::Brownout => init_rtc_store = true, //TODO still required? or via button ignore? to_config_mode = true;
ResetReason::TaskWatchdog => init_rtc_store = true,
ResetReason::DeepSleep => {}
ResetReason::USBPeripheral => {
init_rtc_store = true;
to_config_mode = true; to_config_mode = true;
"core usb uart"
} }
ResetReason::JTAG => init_rtc_store = true, SocResetReason::CoreUsbJtag => "core usb jtag",
SocResetReason::Cpu0JtagCpu => "cpu0 jtag cpu",
},
}; };
log( LOG_ACCESS
.lock()
.await
.log(
LogMessage::ResetReason, LogMessage::ResetReason,
init_rtc_store as u32, init_rtc_store as u32,
to_config_mode as u32, to_config_mode as u32,
"", "",
&format!("{reasons:?}"), &format!("{reasons:?}"),
);
esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode);
let fs_mount_error = esp.mount_file_system().is_err();
let config = esp.load_config();
println!("Init rtc driver");
let mut rtc = Ds323x::new_ds3231(MutexDevice::new(&I2C_DRIVER));
println!("Init rtc eeprom driver");
let eeprom = {
Eeprom24x::new_24x32(
MutexDevice::new(&I2C_DRIVER),
SlaveAddr::Alternative(true, true, true),
) )
}; .await;
esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode)
.await;
let config = esp.load_config().await;
log::info!("Init rtc driver");
let sda = peripherals.GPIO20;
let scl = peripherals.GPIO19;
let i2c = I2c::new(
peripherals.I2C0,
Config::default()
.with_frequency(Rate::from_hz(100))
.with_timeout(BusTimeout::Maximum),
)?
.with_scl(scl)
.with_sda(sda);
let i2c_bus: embassy_sync::blocking_mutex::Mutex<
CriticalSectionRawMutex,
RefCell<I2c<Blocking>>,
> = CriticalSectionMutex::new(RefCell::new(i2c));
I2C_DRIVER.init(i2c_bus).expect("Could not init i2c driver");
let i2c_bus = I2C_DRIVER.get().await;
let rtc_device = I2cDevice::new(&i2c_bus);
let eeprom_device = I2cDevice::new(&i2c_bus);
let mut rtc: Ds323x<
I2cInterface<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>>,
DS3231,
> = Ds323x::new_ds3231(rtc_device);
info!("Init rtc eeprom driver");
let eeprom = Eeprom24x::new_24x32(eeprom_device, SlaveAddr::Alternative(true, true, true));
let rtc_time = rtc.datetime(); let rtc_time = rtc.datetime();
match rtc_time { match rtc_time {
OkStd(tt) => { Ok(tt) => {
println!("Rtc Module reports time at UTC {}", tt); log::info!("Rtc Module reports time at UTC {}", tt);
} }
Err(err) => { Err(err) => {
println!("Rtc Module could not be read {:?}", err); log::info!("Rtc Module could not be read {:?}", err);
} }
} }
let storage = Storage::new(eeprom, Delay::new(1000)); let storage: Storage<
I2cDevice<'static, CriticalSectionRawMutex, I2c<Blocking>>,
B32,
TwoBytes,
No,
Delay,
> = Storage::new(eeprom, Delay::new());
let rtc_module: Box<dyn RTCModuleInteraction + Send> = let rtc_module: Box<dyn RTCModuleInteraction + Send> =
Box::new(DS3231Module { rtc, storage }) as Box<dyn RTCModuleInteraction + Send>; Box::new(DS3231Module { rtc, storage }) as Box<dyn RTCModuleInteraction + Send>;
@@ -296,22 +499,27 @@ impl PlantHal {
match config.hardware.battery { match config.hardware.battery {
BatteryBoardVersion::Disabled => Box::new(NoBatteryMonitor {}), BatteryBoardVersion::Disabled => Box::new(NoBatteryMonitor {}),
BatteryBoardVersion::BQ34Z100G1 => { BatteryBoardVersion::BQ34Z100G1 => {
let battery_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut battery_driver = Bq34z100g1Driver { let mut battery_driver = Bq34z100g1Driver {
i2c: MutexDevice::new(&I2C_DRIVER), i2c: battery_device,
delay: Delay::new(0), delay: Delay::new(),
flash_block_data: [0; 32], flash_block_data: [0; 32],
}; };
let status = print_battery_bq34z100(&mut battery_driver); let status = print_battery_bq34z100(&mut battery_driver);
match status { match status {
OkStd(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
log( LOG_ACCESS
.lock()
.await
.log(
LogMessage::BatteryCommunicationError, LogMessage::BatteryCommunicationError,
0u32, 0u32,
0, 0,
"", "",
&format!("{err:?})"), &format!("{err:?})"),
); )
.await;
} }
} }
Box::new(BQ34Z100G1 { battery_driver }) Box::new(BQ34Z100G1 { battery_driver })
@@ -324,30 +532,34 @@ impl PlantHal {
let board_hal: Box<dyn BoardInteraction + Send> = match config.hardware.board { let board_hal: Box<dyn BoardInteraction + Send> = match config.hardware.board {
BoardVersion::INITIAL => { BoardVersion::INITIAL => {
initial_hal::create_initial_board(free_pins, fs_mount_error, config, esp)? initial_hal::create_initial_board(free_pins, config, esp)?
} }
BoardVersion::V3 => { BoardVersion::V3 => {
v3_hal::create_v3(free_pins, esp, config, battery_interaction, rtc_module)? v3_hal::create_v3(free_pins, esp, config, battery_interaction, rtc_module)?
} }
BoardVersion::V4 => { BoardVersion::V4 => {
v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module)? v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module)
.await?
} }
}; };
HAL { board_hal } HAL { board_hal }
} }
Err(err) => { Err(err) => {
log( LOG_ACCESS
.lock()
.await
.log(
LogMessage::ConfigModeMissingConfig, LogMessage::ConfigModeMissingConfig,
0, 0,
0, 0,
"", "",
&err.to_string(), &err.to_string(),
); )
.await;
HAL { HAL {
board_hal: initial_hal::create_initial_board( board_hal: initial_hal::create_initial_board(
free_pins, free_pins,
fs_mount_error,
PlantControllerConfig::default(), PlantControllerConfig::default(),
esp, esp,
)?, )?,
@@ -358,3 +570,24 @@ impl PlantHal {
Ok(Mutex::new(hal)) Ok(Mutex::new(hal))
} }
} }
pub async fn esp_time() -> DateTime<Utc> {
let guard = TIME_ACCESS.get().await.lock().await;
DateTime::from_timestamp_micros(guard.current_time_us() as i64).unwrap()
}
pub async fn esp_set_time(time: DateTime<FixedOffset>) -> FatResult<()> {
{
let guard = TIME_ACCESS.get().await.lock().await;
guard.set_current_time_us(time.timestamp_micros() as u64);
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.get_rtc_module()
.set_rtc_time(&time.to_utc())
.await
}

View File

@@ -1,132 +1,133 @@
use anyhow::{anyhow, bail}; use crate::hal::Box;
use crate::fat_error::FatResult;
use async_trait::async_trait;
use bincode::config::Configuration; use bincode::config::Configuration;
use bincode::{config, Decode, Encode}; use bincode::{config, Decode, Encode};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use ds323x::ic::DS3231;
use ds323x::interface::I2cInterface;
use ds323x::{DateTimeAccess, Ds323x}; use ds323x::{DateTimeAccess, Ds323x};
use eeprom24x::addr_size::TwoBytes; use eeprom24x::addr_size::TwoBytes;
use eeprom24x::page_size::B32; use eeprom24x::page_size::B32;
use eeprom24x::unique_serial::No; use eeprom24x::unique_serial::No;
use eeprom24x::Storage; use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embedded_hal_bus::i2c::MutexDevice; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embedded_storage::ReadStorage as embedded_storage_ReadStorage; use embedded_storage::{ReadStorage, Storage};
use embedded_storage::Storage as embedded_storage_Storage; use esp_hal::delay::Delay;
use esp_idf_hal::delay::Delay; use esp_hal::i2c::master::I2c;
use esp_idf_hal::i2c::I2cDriver; use esp_hal::Blocking;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::result::Result::Ok as OkStd;
const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC); pub const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC);
const CONFIG: Configuration = config::standard(); const CONFIG: Configuration = config::standard();
//
#[async_trait]
pub trait RTCModuleInteraction { pub trait RTCModuleInteraction {
fn get_backup_info(&mut self) -> anyhow::Result<BackupHeader>; async fn get_backup_info(&mut self) -> FatResult<BackupHeader>;
fn get_backup_config(&mut self) -> anyhow::Result<Vec<u8>>; async fn get_backup_config(&mut self, chunk: usize) -> FatResult<([u8; 32], usize, u16)>;
fn backup_config(&mut self, bytes: &[u8]) -> anyhow::Result<()>; async fn backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()>;
fn get_rtc_time(&mut self) -> anyhow::Result<DateTime<Utc>>; async fn backup_config_finalize(&mut self, crc: u16, length: usize) -> FatResult<()>;
fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> anyhow::Result<()>; async fn get_rtc_time(&mut self) -> FatResult<DateTime<Utc>>;
async fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> FatResult<()>;
} }
//
const BACKUP_HEADER_MAX_SIZE: usize = 64; const BACKUP_HEADER_MAX_SIZE: usize = 64;
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Encode, Decode)] #[derive(Serialize, Deserialize, PartialEq, Debug, Default, Encode, Decode)]
pub struct BackupHeader { pub struct BackupHeader {
pub timestamp: i64, pub timestamp: i64,
crc16: u16, crc16: u16,
pub size: u16, pub size: u16,
} }
//
pub struct DS3231Module {
pub(crate) rtc: Ds323x<
I2cInterface<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>>,
DS3231,
>,
pub struct DS3231Module<'a> { pub(crate) storage: eeprom24x::Storage<
pub(crate) rtc: I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>,
Ds323x<ds323x::interface::I2cInterface<MutexDevice<'a, I2cDriver<'a>>>, ds323x::ic::DS3231>, B32,
TwoBytes,
pub(crate) storage: Storage<MutexDevice<'a, I2cDriver<'a>>, B32, TwoBytes, No, Delay>, No,
Delay,
>,
} }
impl RTCModuleInteraction for DS3231Module<'_> { #[async_trait]
fn get_backup_info(&mut self) -> anyhow::Result<BackupHeader> { impl RTCModuleInteraction for DS3231Module {
async fn get_backup_info(&mut self) -> FatResult<BackupHeader> {
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE]; let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
self.storage self.storage.read(0, &mut header_page_buffer)?;
.read(0, &mut header_page_buffer)
.map_err(|err| anyhow!("Error reading eeprom header {:?}", err))?;
let (header, len): (BackupHeader, usize) = let (header, len): (BackupHeader, usize) =
bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?; bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?;
println!("Raw header is {:?} with size {}", header_page_buffer, len); log::info!("Raw header is {:?} with size {}", header_page_buffer, len);
anyhow::Ok(header) Ok(header)
} }
fn get_backup_config(&mut self) -> anyhow::Result<Vec<u8>> { async fn get_backup_config(&mut self, chunk: usize) -> FatResult<([u8; 32], usize, u16)> {
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE]; let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
self.storage self.storage.read(0, &mut header_page_buffer)?;
.read(0, &mut header_page_buffer)
.map_err(|err| anyhow!("Error reading eeprom header {:?}", err))?;
let (header, _header_size): (BackupHeader, usize) = let (header, _header_size): (BackupHeader, usize) =
bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?; bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?;
let mut data_buffer = vec![0_u8; header.size as usize]; let mut buf = [0_u8; 32];
//read the specified number of bytes after the header let offset = chunk * buf.len() + BACKUP_HEADER_MAX_SIZE;
let end: usize = header.size as usize + BACKUP_HEADER_MAX_SIZE;
let current_end = offset + buf.len();
let chunk_size = if current_end > end {
end - offset
} else {
buf.len()
};
if chunk_size == 0 {
Ok((buf, 0, header.crc16))
} else {
self.storage.read(offset as u32, &mut buf)?;
//&buf[..chunk_size];
Ok((buf, chunk_size, header.crc16))
}
}
async fn backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()> {
//skip header and write after
self.storage self.storage
.read(BACKUP_HEADER_MAX_SIZE as u32, &mut data_buffer) .write((BACKUP_HEADER_MAX_SIZE + offset) as u32, &bytes)?;
.map_err(|err| anyhow!("Error reading eeprom data {:?}", err))?;
let checksum = X25.checksum(&data_buffer); Ok(())
if checksum != header.crc16 {
bail!(
"Invalid checksum, got {} but expected {}",
checksum,
header.crc16
);
} }
anyhow::Ok(data_buffer) async fn backup_config_finalize(&mut self, crc: u16, length: usize) -> FatResult<()> {
}
fn backup_config(&mut self, bytes: &[u8]) -> anyhow::Result<()> {
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE]; let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
let time = self.get_rtc_time()?.timestamp_millis(); let time = self.get_rtc_time().await?.timestamp_millis();
let checksum = X25.checksum(bytes);
let header = BackupHeader { let header = BackupHeader {
crc16: checksum, crc16: crc,
timestamp: time, timestamp: time,
size: bytes.len() as u16, size: length as u16,
}; };
let config = config::standard(); let config = config::standard();
let encoded = bincode::encode_into_slice(&header, &mut header_page_buffer, config)?; let encoded = bincode::encode_into_slice(&header, &mut header_page_buffer, config)?;
println!( log::info!(
"Raw header is {:?} with size {}", "Raw header is {:?} with size {}",
header_page_buffer, encoded header_page_buffer,
encoded
); );
self.storage self.storage.write(0, &header_page_buffer)?;
.write(0, &header_page_buffer) Ok(())
.map_err(|err| anyhow!("Error writing header {:?}", err))?;
//write rest after the header
self.storage
.write(BACKUP_HEADER_MAX_SIZE as u32, &bytes)
.map_err(|err| anyhow!("Error writing body {:?}", err))?;
anyhow::Ok(())
} }
fn get_rtc_time(&mut self) -> anyhow::Result<DateTime<Utc>> { async fn get_rtc_time(&mut self) -> FatResult<DateTime<Utc>> {
match self.rtc.datetime() { Ok(self.rtc.datetime()?.and_utc())
OkStd(rtc_time) => anyhow::Ok(rtc_time.and_utc()),
Err(err) => {
bail!("Error getting rtc time {:?}", err)
}
}
} }
fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> anyhow::Result<()> { async fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> FatResult<()> {
let naive_time = time.naive_utc(); let naive_time = time.naive_utc();
match self.rtc.set_datetime(&naive_time) { Ok(self.rtc.set_datetime(&naive_time)?)
OkStd(_) => anyhow::Ok(()),
Err(err) => {
bail!("Error getting rtc time {:?}", err)
}
}
} }
} }

View File

@@ -1,23 +1,25 @@
use crate::bail;
use crate::fat_error::FatError;
use crate::hal::esp::{hold_disable, hold_enable};
use crate::hal::rtc::RTCModuleInteraction; use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::v3_shift_register::ShiftRegister40;
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::hal::{ use crate::hal::{BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT, TIME_ACCESS};
deep_sleep, BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT, use crate::log::{LogMessage, LOG_ACCESS};
};
use crate::log::{log, LogMessage};
use crate::{ use crate::{
config::PlantControllerConfig, config::PlantControllerConfig,
hal::{battery::BatteryInteraction, esp::Esp}, hal::{battery::BatteryInteraction, esp::Esp},
}; };
use anyhow::{bail, Ok, Result}; use alloc::boxed::Box;
use embedded_hal::digital::OutputPin; use alloc::format;
use esp_idf_hal::{ use alloc::string::ToString;
gpio::{AnyInputPin, IOPin, InputOutput, PinDriver, Pull}, use async_trait::async_trait;
pcnt::{PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex}, use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
}; use embassy_sync::mutex::Mutex;
use esp_idf_sys::{gpio_hold_dis, gpio_hold_en}; use embassy_time::Timer;
use embedded_hal::digital::OutputPin as _;
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
use measurements::{Current, Voltage}; use measurements::{Current, Voltage};
use plant_ctrl2::sipo::ShiftRegister40;
use std::result::Result::Ok as OkStd;
const PUMP8_BIT: usize = 0; const PUMP8_BIT: usize = 0;
const PUMP1_BIT: usize = 1; const PUMP1_BIT: usize = 1;
@@ -66,69 +68,56 @@ const FAULT_2: usize = 23;
const REPEAT_MOIST_MEASURE: usize = 1; const REPEAT_MOIST_MEASURE: usize = 1;
pub struct V3<'a> { pub struct V3<'a> {
config: PlantControllerConfig, config: PlantControllerConfig,
battery_monitor: Box<dyn BatteryInteraction + Send>, battery_monitor: Box<dyn BatteryInteraction + Send>,
rtc_module: Box<dyn RTCModuleInteraction + Send>, rtc_module: Box<dyn RTCModuleInteraction + Send>,
esp: Esp<'a>, esp: Esp<'a>,
shift_register: ShiftRegister40< shift_register:
PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, Mutex<CriticalSectionRawMutex, ShiftRegister40<Output<'a>, Output<'a>, Output<'a>>>,
PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, _shift_register_enable_invert: Output<'a>,
PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
>,
_shift_register_enable_invert:
PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Output>,
tank_sensor: TankSensor<'a>, tank_sensor: TankSensor<'a>,
solar_is_day: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>, solar_is_day: Input<'a>,
light: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, light: Output<'a>,
main_pump: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, main_pump: Output<'a>,
general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, general_fault: Output<'a>,
signal_counter: PcntDriver<'a>,
} }
pub(crate) fn create_v3( pub(crate) fn create_v3(
peripherals: FreePeripherals, peripherals: FreePeripherals<'static>,
esp: Esp<'static>, esp: Esp<'static>,
config: PlantControllerConfig, config: PlantControllerConfig,
battery_monitor: Box<dyn BatteryInteraction + Send>, battery_monitor: Box<dyn BatteryInteraction + Send>,
rtc_module: Box<dyn RTCModuleInteraction + Send>, rtc_module: Box<dyn RTCModuleInteraction + Send>,
) -> Result<Box<dyn BoardInteraction<'static> + Send>> { ) -> Result<Box<dyn BoardInteraction<'static> + Send + 'static>, FatError> {
println!("Start v3"); log::info!("Start v3");
let mut clock = PinDriver::input_output(peripherals.gpio15.downgrade())?; let clock = Output::new(peripherals.gpio15, Level::Low, OutputConfig::default());
clock.set_pull(Pull::Floating)?; let latch = Output::new(peripherals.gpio3, Level::Low, OutputConfig::default());
let mut latch = PinDriver::input_output(peripherals.gpio3.downgrade())?; let data = Output::new(peripherals.gpio23, Level::Low, OutputConfig::default());
latch.set_pull(Pull::Floating)?;
let mut data = PinDriver::input_output(peripherals.gpio23.downgrade())?;
data.set_pull(Pull::Floating)?;
let shift_register = ShiftRegister40::new(clock, latch, data); let shift_register = ShiftRegister40::new(clock, latch, data);
//disable all //disable all
for mut pin in shift_register.decompose() { for mut pin in shift_register.decompose() {
pin.set_low()?; let _ = pin.set_low();
} }
let awake = &mut shift_register.decompose()[AWAKE]; // Set always-on status bits
awake.set_high()?; let _ = shift_register.decompose()[AWAKE].set_high();
let _ = shift_register.decompose()[CHARGING].set_high();
let charging = &mut shift_register.decompose()[CHARGING]; // Multiplexer defaults: ms0..ms3 low, ms4 high (disabled)
charging.set_high()?; let _ = shift_register.decompose()[MS_0].set_low();
let _ = shift_register.decompose()[MS_1].set_low();
let _ = shift_register.decompose()[MS_2].set_low();
let _ = shift_register.decompose()[MS_3].set_low();
let _ = shift_register.decompose()[MS_4].set_high();
let ms0 = &mut shift_register.decompose()[MS_0]; let one_wire_pin = Flex::new(peripherals.gpio18);
ms0.set_low()?; let tank_power_pin = Output::new(peripherals.gpio11, Level::Low, OutputConfig::default());
let ms1 = &mut shift_register.decompose()[MS_1];
ms1.set_low()?;
let ms2 = &mut shift_register.decompose()[MS_2];
ms2.set_low()?;
let ms3 = &mut shift_register.decompose()[MS_3];
ms3.set_low()?;
let ms4 = &mut shift_register.decompose()[MS_4]; let flow_sensor_pin = Input::new(
ms4.set_high()?; peripherals.gpio4,
InputConfig::default().with_pull(Pull::Up),
let one_wire_pin = peripherals.gpio18.downgrade(); );
let tank_power_pin = peripherals.gpio11.downgrade();
let flow_sensor_pin = peripherals.gpio4.downgrade();
let tank_sensor = TankSensor::create( let tank_sensor = TankSensor::create(
one_wire_pin, one_wire_pin,
@@ -139,67 +128,36 @@ pub(crate) fn create_v3(
peripherals.pcnt1, peripherals.pcnt1,
)?; )?;
let mut signal_counter = PcntDriver::new( let solar_is_day = Input::new(peripherals.gpio7, InputConfig::default());
peripherals.pcnt0, let light = Output::new(peripherals.gpio10, Level::Low, OutputConfig::default());
Some(peripherals.gpio22), let mut main_pump = Output::new(peripherals.gpio2, Level::Low, OutputConfig::default());
Option::<AnyInputPin>::None, main_pump.set_low();
Option::<AnyInputPin>::None, let mut general_fault = Output::new(peripherals.gpio6, Level::Low, OutputConfig::default());
Option::<AnyInputPin>::None, general_fault.set_low();
)?;
signal_counter.channel_config( let mut shift_register_enable_invert =
PcntChannel::Channel0, Output::new(peripherals.gpio21, Level::Low, OutputConfig::default());
PinIndex::Pin0, shift_register_enable_invert.set_low();
PinIndex::Pin1,
&PcntChannelConfig {
lctrl_mode: PcntControlMode::Keep,
hctrl_mode: PcntControlMode::Keep,
pos_mode: PcntCountMode::Increment,
neg_mode: PcntCountMode::Hold,
counter_h_lim: i16::MAX,
counter_l_lim: 0,
},
)?;
let mut solar_is_day = PinDriver::input(peripherals.gpio7.downgrade())?;
solar_is_day.set_pull(Pull::Floating)?;
let mut light = PinDriver::input_output(peripherals.gpio10.downgrade())?;
light.set_pull(Pull::Floating)?;
let mut main_pump = PinDriver::input_output(peripherals.gpio2.downgrade())?;
main_pump.set_pull(Pull::Floating)?;
main_pump.set_low()?;
let mut general_fault = PinDriver::input_output(peripherals.gpio6.downgrade())?;
general_fault.set_pull(Pull::Floating)?;
general_fault.set_low()?;
let mut shift_register_enable_invert = PinDriver::output(peripherals.gpio21.downgrade())?;
unsafe { gpio_hold_dis(shift_register_enable_invert.pin()) };
shift_register_enable_invert.set_low()?;
unsafe { gpio_hold_en(shift_register_enable_invert.pin()) };
Ok(Box::new(V3 { Ok(Box::new(V3 {
config, config,
battery_monitor, battery_monitor,
rtc_module, rtc_module,
esp, esp,
shift_register, shift_register: Mutex::new(shift_register),
_shift_register_enable_invert: shift_register_enable_invert, _shift_register_enable_invert: shift_register_enable_invert,
tank_sensor, tank_sensor,
solar_is_day, solar_is_day,
light, light,
main_pump, main_pump,
general_fault, general_fault,
signal_counter,
})) }))
} }
#[async_trait]
impl<'a> BoardInteraction<'a> for V3<'a> { impl<'a> BoardInteraction<'a> for V3<'a> {
fn get_tank_sensor(&mut self) -> Option<&mut TankSensor<'a>> { fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
Some(&mut self.tank_sensor) Ok(&mut self.tank_sensor)
} }
fn get_esp(&mut self) -> &mut Esp<'a> { fn get_esp(&mut self) -> &mut Esp<'a> {
@@ -210,35 +168,46 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
&self.config &self.config
} }
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send + 'static> { fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send> {
&mut self.battery_monitor &mut self.battery_monitor
} }
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> { fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> {
&mut self.rtc_module &mut self.rtc_module
} }
fn set_charge_indicator(&mut self, charging: bool) -> Result<()> { async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
Ok(self.shift_register.decompose()[CHARGING].set_state(charging.into())?) let shift_register = self.shift_register.lock().await;
if charging {
let _ = shift_register.decompose()[CHARGING].set_high();
} else {
let _ = shift_register.decompose()[CHARGING].set_low();
}
Ok(())
} }
fn deep_sleep(&mut self, duration_in_ms: u64) -> ! { async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
let _ = self.shift_register.decompose()[AWAKE].set_low(); let _ = self.shift_register.lock().await.decompose()[AWAKE].set_low();
deep_sleep(duration_in_ms) let guard = TIME_ACCESS.get().await.lock().await;
self.esp.deep_sleep(duration_in_ms, guard)
} }
fn is_day(&self) -> bool { fn is_day(&self) -> bool {
self.solar_is_day.get_level().into() self.solar_is_day.is_high()
} }
fn light(&mut self, enable: bool) -> Result<()> { async fn light(&mut self, enable: bool) -> Result<(), FatError> {
unsafe { gpio_hold_dis(self.light.pin()) }; hold_disable(10);
self.light.set_state(enable.into())?; if enable {
unsafe { gpio_hold_en(self.light.pin()) }; self.light.set_high();
} else {
self.light.set_low();
}
hold_enable(10);
Ok(()) Ok(())
} }
fn pump(&mut self, plant: usize, enable: bool) -> Result<()> { async fn pump(&mut self, plant: usize, enable: bool) -> Result<(), FatError> {
if enable { if enable {
self.main_pump.set_high()?; self.main_pump.set_high();
} }
let index = match plant { let index = match plant {
@@ -250,21 +219,26 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
5 => PUMP6_BIT, 5 => PUMP6_BIT,
6 => PUMP7_BIT, 6 => PUMP7_BIT,
7 => PUMP8_BIT, 7 => PUMP8_BIT,
_ => bail!("Invalid pump {plant}",), _ => bail!("Invalid pump {plant}"),
}; };
self.shift_register.decompose()[index].set_state(enable.into())?; let shift_register = self.shift_register.lock().await;
if enable {
let _ = shift_register.decompose()[index].set_high();
} else {
let _ = shift_register.decompose()[index].set_low();
}
if !enable { if !enable {
self.main_pump.set_low()?; self.main_pump.set_low();
} }
Ok(()) Ok(())
} }
fn pump_current(&mut self, _plant: usize) -> Result<Current> { async fn pump_current(&mut self, _plant: usize) -> Result<Current, FatError> {
bail!("Not implemented in v3") bail!("Not implemented in v3")
} }
fn fault(&mut self, plant: usize, enable: bool) -> Result<()> { async fn fault(&mut self, plant: usize, enable: bool) -> Result<(), FatError> {
let index = match plant { let index = match plant {
0 => FAULT_1, 0 => FAULT_1,
1 => FAULT_2, 1 => FAULT_2,
@@ -276,17 +250,25 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
7 => FAULT_8, 7 => FAULT_8,
_ => panic!("Invalid plant id {}", plant), _ => panic!("Invalid plant id {}", plant),
}; };
self.shift_register.decompose()[index].set_state(enable.into())?; let shift_register = self.shift_register.lock().await;
if enable {
let _ = shift_register.decompose()[index].set_high();
} else {
let _ = shift_register.decompose()[index].set_low();
}
Ok(()) Ok(())
} }
fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32> { async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError> {
let mut results = [0_f32; REPEAT_MOIST_MEASURE]; let mut results = [0_f32; REPEAT_MOIST_MEASURE];
for repeat in 0..REPEAT_MOIST_MEASURE { for repeat in 0..REPEAT_MOIST_MEASURE {
self.signal_counter.counter_pause()?; //self.signal_counter.counter_pause()?;
self.signal_counter.counter_clear()?; //self.signal_counter.counter_clear()?;
//Disable all //Disable all
self.shift_register.decompose()[MS_4].set_high()?; {
let shift_register = self.shift_register.lock().await;
shift_register.decompose()[MS_4].set_high()?;
}
let sensor_channel = match sensor { let sensor_channel = match sensor {
Sensor::A => match plant { Sensor::A => match plant {
@@ -314,10 +296,12 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
}; };
let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 }; let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 };
let pin_0 = &mut self.shift_register.decompose()[MS_0]; {
let pin_1 = &mut self.shift_register.decompose()[MS_1]; let shift_register = self.shift_register.lock().await;
let pin_2 = &mut self.shift_register.decompose()[MS_2]; let pin_0 = &mut shift_register.decompose()[MS_0];
let pin_3 = &mut self.shift_register.decompose()[MS_3]; let pin_1 = &mut shift_register.decompose()[MS_1];
let pin_2 = &mut shift_register.decompose()[MS_2];
let pin_3 = &mut shift_register.decompose()[MS_3];
if is_bit_set(0) { if is_bit_set(0) {
pin_0.set_high()?; pin_0.set_high()?;
} else { } else {
@@ -339,29 +323,36 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
pin_3.set_low()?; pin_3.set_low()?;
} }
self.shift_register.decompose()[MS_4].set_low()?; shift_register.decompose()[MS_4].set_low()?;
self.shift_register.decompose()[SENSOR_ON].set_high()?; shift_register.decompose()[SENSOR_ON].set_high()?;
}
let measurement = 100; //how long to measure and then extrapolate to hz let measurement = 100; //how long to measure and then extrapolate to hz
let factor = 1000f32 / measurement as f32; //scale raw cound by this number to get hz let factor = 1000f32 / measurement as f32; //scale raw cound by this number to get hz
//give some time to stabilize //give some time to stabilize
self.esp.delay.delay_ms(10); Timer::after_millis(10).await;
self.signal_counter.counter_resume()?; //self.signal_counter.counter_resume()?;
self.esp.delay.delay_ms(measurement); Timer::after_millis(measurement).await;
self.signal_counter.counter_pause()?; //self.signal_counter.counter_pause()?;
self.shift_register.decompose()[MS_4].set_high()?; {
self.shift_register.decompose()[SENSOR_ON].set_low()?; let shift_register = self.shift_register.lock().await;
self.esp.delay.delay_ms(10); shift_register.decompose()[MS_4].set_high()?;
let unscaled = self.signal_counter.get_counter_value()? as i32; shift_register.decompose()[SENSOR_ON].set_low()?;
}
Timer::after_millis(10).await;
let unscaled = 1337; //self.signal_counter.get_counter_value()? as i32;
let hz = unscaled as f32 * factor; let hz = unscaled as f32 * factor;
log( LOG_ACCESS
.lock()
.await
.log(
LogMessage::RawMeasure, LogMessage::RawMeasure,
unscaled as u32, unscaled as u32,
hz as u32, hz as u32,
&plant.to_string(), &plant.to_string(),
&format!("{sensor:?}"), &format!("{sensor:?}"),
); )
.await;
results[repeat] = hz; results[repeat] = hz;
} }
results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
@@ -371,66 +362,67 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
Ok(median) Ok(median)
} }
fn general_fault(&mut self, enable: bool) { async fn general_fault(&mut self, enable: bool) {
unsafe { gpio_hold_dis(self.general_fault.pin()) }; hold_disable(6);
self.general_fault.set_state(enable.into()).unwrap(); if enable {
unsafe { gpio_hold_en(self.general_fault.pin()) }; self.general_fault.set_high();
} else {
self.general_fault.set_low();
}
hold_enable(6);
} }
fn test(&mut self) -> Result<()> { async fn test(&mut self) -> Result<(), FatError> {
self.general_fault(true); self.general_fault(true).await;
self.esp.delay.delay_ms(100); Timer::after_millis(100).await;
self.general_fault(false); self.general_fault(false).await;
self.esp.delay.delay_ms(100); Timer::after_millis(100).await;
self.light(true)?; self.light(true).await?;
self.esp.delay.delay_ms(500); Timer::after_millis(500).await;
self.light(false)?;
self.esp.delay.delay_ms(500); self.light(false).await?;
Timer::after_millis(500).await;
for i in 0..PLANT_COUNT { for i in 0..PLANT_COUNT {
self.fault(i, true)?; self.fault(i, true).await?;
self.esp.delay.delay_ms(500); Timer::after_millis(500).await;
self.fault(i, false)?; self.fault(i, false).await?;
self.esp.delay.delay_ms(500); Timer::after_millis(500).await;
} }
for i in 0..PLANT_COUNT { for i in 0..PLANT_COUNT {
self.pump(i, true)?; self.pump(i, true).await?;
self.esp.delay.delay_ms(100); Timer::after_millis(100).await;
self.pump(i, false)?; self.pump(i, false).await?;
self.esp.delay.delay_ms(100); Timer::after_millis(100).await;
} }
for plant in 0..PLANT_COUNT { for plant in 0..PLANT_COUNT {
let a = self.measure_moisture_hz(plant, Sensor::A); let a = self.measure_moisture_hz(plant, Sensor::A).await;
let b = self.measure_moisture_hz(plant, Sensor::B); let b = self.measure_moisture_hz(plant, Sensor::B).await;
let aa = match a { let aa = match a {
OkStd(a) => a as u32, Ok(a) => a as u32,
Err(_) => u32::MAX, Err(_) => u32::MAX,
}; };
let bb = match b { let bb = match b {
OkStd(b) => b as u32, Ok(b) => b as u32,
Err(_) => u32::MAX, Err(_) => u32::MAX,
}; };
log(LogMessage::TestSensor, aa, bb, &plant.to_string(), ""); LOG_ACCESS
.lock()
.await
.log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "")
.await;
} }
self.esp.delay.delay_ms(10); Timer::after_millis(10).await;
Ok(()) Ok(())
} }
fn set_config(&mut self, config: PlantControllerConfig) -> Result<()> { fn set_config(&mut self, config: PlantControllerConfig) {
self.config = config; self.config = config;
self.esp.save_config(&self.config)?;
anyhow::Ok(())
} }
fn get_mptt_voltage(&mut self) -> Result<Voltage> { async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError> {
//assuming module to work, these are the hardware set values bail!("Not implemented in v3")
if self.is_day() { }
Ok(Voltage::from_volts(15_f64)) async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
} else { bail!("Not implemented in v3")
Ok(Voltage::from_volts(0_f64))
}
}
fn get_mptt_current(&mut self) -> Result<Current> {
bail!("Board does not have current sensor")
} }
} }

View File

@@ -1,12 +1,13 @@
//! Serial-in parallel-out shift register //! Serial-in parallel-out shift register
#![allow(warnings)]
use core::cell::RefCell; use core::cell::RefCell;
use core::convert::Infallible;
use core::iter::Iterator;
use core::mem::{self, MaybeUninit}; use core::mem::{self, MaybeUninit};
use std::convert::Infallible; use core::result::{Result, Result::Ok};
use embedded_hal::digital::OutputPin;
use hal::digital::OutputPin; trait ShiftRegisterInternal: Send {
trait ShiftRegisterInternal {
fn update(&self, index: usize, command: bool) -> Result<(), ()>; fn update(&self, index: usize, command: bool) -> Result<(), ()>;
} }
@@ -46,9 +47,9 @@ macro_rules! ShiftRegisterBuilder {
/// Serial-in parallel-out shift register /// Serial-in parallel-out shift register
pub struct $name<Pin1, Pin2, Pin3> pub struct $name<Pin1, Pin2, Pin3>
where where
Pin1: OutputPin, Pin1: OutputPin + Send,
Pin2: OutputPin, Pin2: OutputPin + Send,
Pin3: OutputPin, Pin3: OutputPin + Send,
{ {
clock: RefCell<Pin1>, clock: RefCell<Pin1>,
latch: RefCell<Pin2>, latch: RefCell<Pin2>,
@@ -58,9 +59,9 @@ macro_rules! ShiftRegisterBuilder {
impl<Pin1, Pin2, Pin3> ShiftRegisterInternal for $name<Pin1, Pin2, Pin3> impl<Pin1, Pin2, Pin3> ShiftRegisterInternal for $name<Pin1, Pin2, Pin3>
where where
Pin1: OutputPin, Pin1: OutputPin + Send,
Pin2: OutputPin, Pin2: OutputPin + Send,
Pin3: OutputPin, Pin3: OutputPin + Send,
{ {
/// Sets the value of the shift register output at `index` to value `command` /// Sets the value of the shift register output at `index` to value `command`
fn update(&self, index: usize, command: bool) -> Result<(), ()> { fn update(&self, index: usize, command: bool) -> Result<(), ()> {
@@ -85,9 +86,9 @@ macro_rules! ShiftRegisterBuilder {
impl<Pin1, Pin2, Pin3> $name<Pin1, Pin2, Pin3> impl<Pin1, Pin2, Pin3> $name<Pin1, Pin2, Pin3>
where where
Pin1: OutputPin, Pin1: OutputPin + Send,
Pin2: OutputPin, Pin2: OutputPin + Send,
Pin3: OutputPin, Pin3: OutputPin + Send,
{ {
/// Creates a new SIPO shift register from clock, latch, and data output pins /// Creates a new SIPO shift register from clock, latch, and data output pins
pub fn new(clock: Pin1, latch: Pin2, data: Pin3) -> Self { pub fn new(clock: Pin1, latch: Pin2, data: Pin3) -> Self {

View File

@@ -1,43 +1,78 @@
use crate::config::PlantControllerConfig; use crate::config::PlantControllerConfig;
use crate::hal::battery::BatteryInteraction; use crate::hal::battery::BatteryInteraction;
use crate::hal::esp::Esp; use crate::hal::esp::{hold_disable, hold_enable, Esp};
use crate::hal::rtc::RTCModuleInteraction; use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::v4_sensor::SensorImpl;
use crate::hal::v4_sensor::SensorInteraction;
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::hal::{ use crate::hal::{BoardInteraction, FreePeripherals, Sensor, I2C_DRIVER, PLANT_COUNT, TIME_ACCESS};
deep_sleep, BoardInteraction, FreePeripherals, Sensor, I2C_DRIVER, PLANT_COUNT use alloc::boxed::Box;
}; use alloc::string::ToString;
use crate::log::{log, LogMessage}; use async_trait::async_trait;
use anyhow::bail; use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embedded_hal::digital::OutputPin; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embedded_hal_bus::i2c::MutexDevice; use embassy_time::Timer;
use esp_idf_hal::gpio::{AnyInputPin, IOPin, InputOutput, Output, PinDriver, Pull}; use esp_hal::{twai, Blocking};
use esp_idf_hal::i2c::I2cDriver; //use embedded_hal_bus::i2c::MutexDevice;
use esp_idf_hal::pcnt::{ use crate::bail;
PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex, use crate::fat_error::{FatError, FatResult};
}; use crate::hal::v4_sensor::{SensorImpl, SensorInteraction};
use esp_idf_sys::{gpio_hold_dis, gpio_hold_en}; use crate::log::{LogMessage, LOG_ACCESS};
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
use esp_hal::i2c::master::I2c;
use esp_hal::pcnt::Pcnt;
use esp_hal::twai::{EspTwaiFrame, StandardId, TwaiMode};
use esp_println::println;
use ina219::address::{Address, Pin}; use ina219::address::{Address, Pin};
use ina219::calibration::UnCalibrated; use ina219::calibration::UnCalibrated;
use ina219::configuration::{Configuration, OperatingMode}; use ina219::configuration::{Configuration, OperatingMode, Resolution};
use ina219::SyncIna219; use ina219::SyncIna219;
use measurements::{Current, Resistance, Voltage}; use measurements::Resistance;
use measurements::{Current, Voltage};
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface}; use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
use std::result::Result::Ok as OkStd;
use embedded_can::Frame; const MPPT_CURRENT_SHUNT_OHMS: f64 = 0.05_f64;
use embedded_can::StandardId; const TWAI_BAUDRATE: twai::BaudRate = twai::BaudRate::B125K;
use esp_idf_hal::can;
pub enum Charger<'a> { pub enum Charger<'a> {
SolarMpptV1 { SolarMpptV1 {
mppt_ina: SyncIna219<MutexDevice<'a, I2cDriver<'a>>, UnCalibrated>, mppt_ina: SyncIna219<
solar_is_day: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>, I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>,
charge_indicator: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, UnCalibrated,
>,
solar_is_day: Input<'a>,
charge_indicator: Output<'a>,
}, },
ErrorInit {}, ErrorInit {},
} }
impl<'a> Charger<'a> {
pub(crate) fn get_mppt_current(&mut self) -> FatResult<Current> {
match self {
Charger::SolarMpptV1 { mppt_ina, .. } => {
let v = mppt_ina.shunt_voltage()?;
let shunt_voltage = Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64);
let shut_value = Resistance::from_ohms(MPPT_CURRENT_SHUNT_OHMS);
let current = shunt_voltage.as_volts() / shut_value.as_ohms();
Ok(Current::from_amperes(current))
}
Charger::ErrorInit { .. } => {
bail!("hardware error during init");
}
}
}
pub(crate) fn get_mptt_voltage(&mut self) -> FatResult<Voltage> {
match self {
Charger::SolarMpptV1 { mppt_ina, .. } => {
let v = mppt_ina.bus_voltage()?;
Ok(Voltage::from_millivolts(v.voltage_mv() as f64))
}
Charger::ErrorInit { .. } => {
bail!("hardware error during init");
}
}
}
}
impl Charger<'_> { impl Charger<'_> {
pub(crate) fn power_save(&mut self) { pub(crate) fn power_save(&mut self) {
match self { match self {
@@ -52,7 +87,7 @@ impl Charger<'_> {
operating_mode: OperatingMode::PowerDown, operating_mode: OperatingMode::PowerDown,
}) })
.map_err(|e| { .map_err(|e| {
println!( log::info!(
"Error setting ina mppt configuration during deep sleep preparation{:?}", "Error setting ina mppt configuration during deep sleep preparation{:?}",
e e
); );
@@ -61,12 +96,12 @@ impl Charger<'_> {
_ => {} _ => {}
} }
} }
fn set_charge_indicator(&mut self, charging: bool) -> anyhow::Result<()> { fn set_charge_indicator(&mut self, charging: bool) -> FatResult<()> {
match self { match self {
Self::SolarMpptV1 { Self::SolarMpptV1 {
charge_indicator, .. charge_indicator, ..
} => { } => {
charge_indicator.set_state(charging.into())?; charge_indicator.set_level(charging.into());
} }
_ => {} _ => {}
} }
@@ -75,37 +110,10 @@ impl Charger<'_> {
fn is_day(&self) -> bool { fn is_day(&self) -> bool {
match self { match self {
Charger::SolarMpptV1 { solar_is_day, .. } => solar_is_day.get_level().into(), Charger::SolarMpptV1 { solar_is_day, .. } => solar_is_day.is_high(),
_ => true, _ => true,
} }
} }
fn get_mptt_voltage(&mut self) -> anyhow::Result<Voltage> {
let voltage = match self {
Charger::SolarMpptV1 { mppt_ina, .. } => mppt_ina
.bus_voltage()
.map(|v| Voltage::from_millivolts(v.voltage_mv() as f64))?,
_ => {
bail!("hardware error during init")
}
};
Ok(voltage)
}
fn get_mptt_current(&mut self) -> anyhow::Result<Current> {
let current = match self {
Charger::SolarMpptV1 { mppt_ina, .. } => mppt_ina.shunt_voltage().map(|v| {
let shunt_voltage = Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64);
let shut_value = Resistance::from_ohms(0.05_f64);
let current = shunt_voltage.as_volts() / shut_value.as_ohms();
Current::from_amperes(current)
})?,
_ => {
bail!("hardware error during init")
}
};
Ok(current)
}
} }
pub struct V4<'a> { pub struct V4<'a> {
@@ -116,40 +124,41 @@ pub struct V4<'a> {
battery_monitor: Box<dyn BatteryInteraction + Send>, battery_monitor: Box<dyn BatteryInteraction + Send>,
config: PlantControllerConfig, config: PlantControllerConfig,
awake: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, Output>, awake: Output<'a>,
light: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, light: Output<'a>,
general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>, general_fault: Output<'a>,
pump_expander: Pca9535Immediate<MutexDevice<'a, I2cDriver<'a>>>, pump_expander: Pca9535Immediate<I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>>,
pump_ina: Option<SyncIna219<MutexDevice<'a, I2cDriver<'a>>, UnCalibrated>>, pump_ina: Option<
sensor: SensorImpl<'a>, SyncIna219<I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>, UnCalibrated>,
extra1: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, Output>, >,
extra2: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, Output>, sensor: SensorImpl,
extra1: Output<'a>,
extra2: Output<'a>,
} }
pub(crate) fn create_v4( pub(crate) async fn create_v4(
peripherals: FreePeripherals, peripherals: FreePeripherals<'static>,
esp: Esp<'static>, esp: Esp<'static>,
config: PlantControllerConfig, config: PlantControllerConfig,
battery_monitor: Box<dyn BatteryInteraction + Send>, battery_monitor: Box<dyn BatteryInteraction + Send>,
rtc_module: Box<dyn RTCModuleInteraction + Send>, rtc_module: Box<dyn RTCModuleInteraction + Send>,
) -> anyhow::Result<Box<dyn BoardInteraction<'static> + Send + 'static>> { ) -> Result<Box<dyn BoardInteraction<'static> + Send + 'static>, FatError> {
println!("Start v4"); log::info!("Start v4");
let mut awake = PinDriver::output(peripherals.gpio21.downgrade())?; let mut awake = Output::new(peripherals.gpio21, Level::High, OutputConfig::default());
awake.set_high()?; awake.set_high();
let mut general_fault = PinDriver::input_output(peripherals.gpio23.downgrade())?; let mut general_fault = Output::new(peripherals.gpio23, Level::Low, OutputConfig::default());
general_fault.set_pull(Pull::Floating)?; general_fault.set_low();
general_fault.set_low()?;
let mut extra1 = PinDriver::output(peripherals.gpio6.downgrade())?; let extra1 = Output::new(peripherals.gpio6, Level::Low, OutputConfig::default());
extra1.set_low()?; let extra2 = Output::new(peripherals.gpio15, Level::Low, OutputConfig::default());
let mut extra2 = PinDriver::output(peripherals.gpio15.downgrade())?; let one_wire_pin = Flex::new(peripherals.gpio18);
extra2.set_low()?; let tank_power_pin = Output::new(peripherals.gpio11, Level::Low, OutputConfig::default());
let flow_sensor_pin = Input::new(
let one_wire_pin = peripherals.gpio18.downgrade(); peripherals.gpio4,
let tank_power_pin = peripherals.gpio11.downgrade(); InputConfig::default().with_pull(Pull::Up),
let flow_sensor_pin = peripherals.gpio4.downgrade(); );
let tank_sensor = TankSensor::create( let tank_sensor = TankSensor::create(
one_wire_pin, one_wire_pin,
@@ -160,32 +169,33 @@ pub(crate) fn create_v4(
peripherals.pcnt1, peripherals.pcnt1,
)?; )?;
let mut sensor_expander = Pca9535Immediate::new(MutexDevice::new(&I2C_DRIVER), 34); let sensor_expander_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut sensor_expander = Pca9535Immediate::new(sensor_expander_device, 34);
let sensor = match sensor_expander.pin_into_output(GPIOBank::Bank0, 0) { let sensor = match sensor_expander.pin_into_output(GPIOBank::Bank0, 0) {
Ok(_) => { Ok(_) => {
println!("SensorExpander answered"); log::info!("SensorExpander answered");
//pulse counter version //pulse counter version
let mut signal_counter = PcntDriver::new( // let mut signal_counter = PcntDriver::new(
peripherals.pcnt0, // peripherals.pcnt0,
Some(peripherals.gpio22), // Some(peripherals.gpio22),
Option::<AnyInputPin>::None, // Option::<AnyInputPin>::None,
Option::<AnyInputPin>::None, // Option::<AnyInputPin>::None,
Option::<AnyInputPin>::None, // Option::<AnyInputPin>::None,
)?; // )?;
//
signal_counter.channel_config( // signal_counter.channel_config(
PcntChannel::Channel0, // PcntChannel::Channel0,
PinIndex::Pin0, // PinIndex::Pin0,
PinIndex::Pin1, // PinIndex::Pin1,
&PcntChannelConfig { // &PcntChannelConfig {
lctrl_mode: PcntControlMode::Keep, // lctrl_mode: PcntControlMode::Keep,
hctrl_mode: PcntControlMode::Keep, // hctrl_mode: PcntControlMode::Keep,
pos_mode: PcntCountMode::Increment, // pos_mode: PcntCountMode::Increment,
neg_mode: PcntCountMode::Hold, // neg_mode: PcntCountMode::Hold,
counter_h_lim: i16::MAX, // counter_h_lim: i16::MAX,
counter_l_lim: 0, // counter_l_lim: 0,
}, // },
)?; // )?;
for pin in 0..8 { for pin in 0..8 {
let _ = sensor_expander.pin_into_output(GPIOBank::Bank0, pin); let _ = sensor_expander.pin_into_output(GPIOBank::Bank0, pin);
@@ -195,45 +205,38 @@ pub(crate) fn create_v4(
} }
SensorImpl::PulseCounter { SensorImpl::PulseCounter {
signal_counter, // signal_counter,
sensor_expander, sensor_expander,
} }
} }
Err(_) => { Err(_) => {
println!("Can bus mode "); log::info!("Can bus mode ");
let timing = can::config::Timing::B25K; let twai_config = twai::TwaiConfiguration::new(
let config = can::config::Config::new().timing(timing); peripherals.twai,
let can = can::CanDriver::new(peripherals.can, peripherals.gpio0, peripherals.gpio2, &config).unwrap(); peripherals.gpio0,
peripherals.gpio2,
TWAI_BAUDRATE,
TwaiMode::Normal,
);
let mut twai = twai_config.start();
let frame = EspTwaiFrame::new(StandardId::ZERO, &[1, 2, 3]).unwrap();
let frame = StandardId::new(0x042).unwrap(); twai.transmit(&frame).unwrap();
let tx_frame = Frame::new(frame, &[0, 1, 2, 3, 4, 5, 6, 7]).unwrap();
can.transmit(&tx_frame, 1000).unwrap();
if let Ok(rx_frame) = can.receive(1000) { // let frame = twai.receive().unwrap();
println!("rx {:}:", rx_frame); println!("Received a frame: {frame:?}");
}
//can bus version //can bus version
SensorImpl::CanBus { SensorImpl::CanBus { twai }
can
}
} }
}; };
let solar_is_day = Input::new(peripherals.gpio7, InputConfig::default());
let light = Output::new(peripherals.gpio10, Level::Low, Default::default());
let charge_indicator = Output::new(peripherals.gpio3, Level::Low, Default::default());
let pump_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut pump_expander = Pca9535Immediate::new(pump_device, 32);
let mut solar_is_day = PinDriver::input(peripherals.gpio7.downgrade())?;
solar_is_day.set_pull(Pull::Floating)?;
let mut light = PinDriver::input_output(peripherals.gpio10.downgrade())?;
light.set_pull(Pull::Floating)?;
let mut charge_indicator = PinDriver::input_output(peripherals.gpio3.downgrade())?;
charge_indicator.set_pull(Pull::Floating)?;
charge_indicator.set_low()?;
let mut pump_expander = Pca9535Immediate::new(MutexDevice::new(&I2C_DRIVER), 32);
for pin in 0..8 { for pin in 0..8 {
let _ = pump_expander.pin_into_output(GPIOBank::Bank0, pin); let _ = pump_expander.pin_into_output(GPIOBank::Bank0, pin);
let _ = pump_expander.pin_into_output(GPIOBank::Bank1, pin); let _ = pump_expander.pin_into_output(GPIOBank::Bank1, pin);
@@ -241,15 +244,37 @@ pub(crate) fn create_v4(
let _ = pump_expander.pin_set_low(GPIOBank::Bank1, pin); let _ = pump_expander.pin_set_low(GPIOBank::Bank1, pin);
} }
let mppt_current = I2cDevice::new(I2C_DRIVER.get().await);
let mppt_ina = match SyncIna219::new(mppt_current, Address::from_pins(Pin::Vcc, Pin::Gnd)) {
Ok(mut ina) => {
// Prefer higher averaging for more stable readings
let _ = ina.set_configuration(Configuration {
reset: Default::default(),
bus_voltage_range: Default::default(),
shunt_voltage_range: Default::default(),
bus_resolution: Default::default(),
shunt_resolution: Resolution::Avg128,
operating_mode: Default::default(),
});
Some(ina)
}
Err(err) => {
log::info!("Error creating mppt ina: {:?}", err);
None
}
};
let pump_current_dev = I2cDevice::new(I2C_DRIVER.get().await);
let mppt_ina = SyncIna219::new( let pump_ina = match SyncIna219::new(pump_current_dev, Address::from_pins(Pin::Gnd, Pin::Sda)) {
MutexDevice::new(&I2C_DRIVER), Ok(ina) => Some(ina),
Address::from_pins(Pin::Vcc, Pin::Gnd), Err(err) => {
); log::info!("Error creating pump ina: {:?}", err);
None
}
};
let charger = match mppt_ina { let charger = match mppt_ina {
Ok(mut mppt_ina) => { Some(mut mppt_ina) => {
mppt_ina.set_configuration(Configuration { mppt_ina.set_configuration(Configuration {
reset: Default::default(), reset: Default::default(),
bus_voltage_range: Default::default(), bus_voltage_range: Default::default(),
@@ -265,18 +290,7 @@ pub(crate) fn create_v4(
charge_indicator, charge_indicator,
} }
} }
Err(_) => Charger::ErrorInit {}, None => Charger::ErrorInit {},
};
let pump_ina = match SyncIna219::new(
MutexDevice::new(&I2C_DRIVER),
Address::from_pins(Pin::Gnd, Pin::Sda),
) {
Ok(pump_ina) => Some(pump_ina),
Err(err) => {
println!("Error creating pump ina: {:?}", err);
None
}
}; };
let v = V4 { let v = V4 {
@@ -286,10 +300,11 @@ pub(crate) fn create_v4(
tank_sensor, tank_sensor,
light, light,
general_fault, general_fault,
pump_ina, //pump_ina,
pump_expander, pump_expander,
config, config,
battery_monitor, battery_monitor,
pump_ina,
charger, charger,
extra1, extra1,
extra2, extra2,
@@ -298,9 +313,10 @@ pub(crate) fn create_v4(
Ok(Box::new(v)) Ok(Box::new(v))
} }
#[async_trait]
impl<'a> BoardInteraction<'a> for V4<'a> { impl<'a> BoardInteraction<'a> for V4<'a> {
fn get_tank_sensor(&mut self) -> Option<&mut TankSensor<'a>> { fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
Some(&mut self.tank_sensor) Ok(&mut self.tank_sensor)
} }
fn get_esp(&mut self) -> &mut Esp<'a> { fn get_esp(&mut self) -> &mut Esp<'a> {
@@ -319,47 +335,54 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
&mut self.rtc_module &mut self.rtc_module
} }
fn set_charge_indicator(&mut self, charging: bool) -> anyhow::Result<()> { async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
self.charger.set_charge_indicator(charging) self.charger.set_charge_indicator(charging)
} }
fn deep_sleep(&mut self, duration_in_ms: u64) -> ! { async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
self.awake.set_low().unwrap(); self.awake.set_low();
self.charger.power_save(); self.charger.power_save();
deep_sleep(duration_in_ms); let rtc = TIME_ACCESS.get().await.lock().await;
self.esp.deep_sleep(duration_in_ms, rtc);
} }
fn is_day(&self) -> bool { fn is_day(&self) -> bool {
self.charger.is_day() self.charger.is_day()
} }
fn light(&mut self, enable: bool) -> anyhow::Result<()> { async fn light(&mut self, enable: bool) -> Result<(), FatError> {
unsafe { gpio_hold_dis(self.light.pin()) }; hold_disable(10);
self.light.set_state(enable.into())?; self.light.set_level(enable.into());
unsafe { gpio_hold_en(self.light.pin()) }; hold_enable(10);
anyhow::Ok(()) Ok(())
} }
fn pump(&mut self, plant: usize, enable: bool) -> anyhow::Result<()> { async fn pump(&mut self, plant: usize, enable: bool) -> FatResult<()> {
if enable { if enable {
self.pump_expander self.pump_expander
.pin_set_high(GPIOBank::Bank0, plant.try_into()?)?; .pin_set_high(GPIOBank::Bank0, plant as u8)?;
} else { } else {
self.pump_expander self.pump_expander
.pin_set_low(GPIOBank::Bank0, plant.try_into()?)?; .pin_set_low(GPIOBank::Bank0, plant as u8)?;
} }
anyhow::Ok(()) Ok(())
} }
fn pump_current(&mut self, _plant: usize) -> anyhow::Result<Current> { async fn pump_current(&mut self, _plant: usize) -> Result<Current, FatError> {
//sensore is shared for all pumps, ignore plant id // sensor is shared for all pumps, ignore plant id
match self.pump_ina.as_mut() { match self.pump_ina.as_mut() {
None => { None => {
bail!("pump current sensor not available"); bail!("pump current sensor not available");
} }
Some(pump_ina) => { Some(pump_ina) => {
let v = pump_ina.shunt_voltage().map(|v| { let v = pump_ina
let shunt_voltage = Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64); .shunt_voltage()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
.map(|v| {
let shunt_voltage =
Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64);
let shut_value = Resistance::from_ohms(0.05_f64); let shut_value = Resistance::from_ohms(0.05_f64);
let current = shunt_voltage.as_volts() / shut_value.as_ohms(); let current = shunt_voltage.as_volts() / shut_value.as_ohms();
Current::from_amperes(current) Current::from_amperes(current)
@@ -369,76 +392,78 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
} }
} }
fn fault(&mut self, plant: usize, enable: bool) -> anyhow::Result<()> { async fn fault(&mut self, plant: usize, enable: bool) -> FatResult<()> {
if enable { if enable {
self.pump_expander self.pump_expander
.pin_set_high(GPIOBank::Bank1, plant.try_into()?)? .pin_set_high(GPIOBank::Bank1, plant as u8)?;
} else { } else {
self.pump_expander self.pump_expander
.pin_set_low(GPIOBank::Bank1, plant.try_into()?)? .pin_set_low(GPIOBank::Bank1, plant as u8)?;
} }
anyhow::Ok(()) Ok(())
} }
fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> anyhow::Result<f32> { async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError> {
self.sensor.measure_moisture_hz(plant, sensor) self.sensor.measure_moisture_hz(plant, sensor).await
} }
fn general_fault(&mut self, enable: bool) { async fn general_fault(&mut self, enable: bool) {
unsafe { gpio_hold_dis(self.general_fault.pin()) }; hold_disable(23);
self.general_fault.set_state(enable.into()).unwrap(); self.general_fault.set_level(enable.into());
unsafe { gpio_hold_en(self.general_fault.pin()) }; hold_enable(23);
} }
fn test(&mut self) -> anyhow::Result<()> { async fn test(&mut self) -> Result<(), FatError> {
self.general_fault(true); self.general_fault(true).await;
self.esp.delay.delay_ms(100); Timer::after_millis(100).await;
self.general_fault(false); self.general_fault(false).await;
self.esp.delay.delay_ms(500); Timer::after_millis(500).await;
self.light(true)?; self.light(true).await?;
self.esp.delay.delay_ms(500); Timer::after_millis(500).await;
self.light(false)?; self.light(false).await?;
self.esp.delay.delay_ms(500); Timer::after_millis(500).await;
for i in 0..PLANT_COUNT { for i in 0..PLANT_COUNT {
self.fault(i, true)?; self.fault(i, true).await?;
self.esp.delay.delay_ms(500); Timer::after_millis(500).await;
self.fault(i, false)?; self.fault(i, false).await?;
self.esp.delay.delay_ms(500); Timer::after_millis(500).await;
} }
for i in 0..PLANT_COUNT { for i in 0..PLANT_COUNT {
self.pump(i, true)?; self.pump(i, true).await?;
self.esp.delay.delay_ms(100); Timer::after_millis(100).await;
self.pump(i, false)?; self.pump(i, false).await?;
self.esp.delay.delay_ms(100); Timer::after_millis(100).await;
} }
for plant in 0..PLANT_COUNT { for plant in 0..PLANT_COUNT {
let a = self.measure_moisture_hz(plant, Sensor::A); let a = self.measure_moisture_hz(plant, Sensor::A).await;
let b = self.measure_moisture_hz(plant, Sensor::B); let b = self.measure_moisture_hz(plant, Sensor::B).await;
let aa = match a { let aa = match a {
OkStd(a) => a as u32, Ok(a) => a as u32,
Err(_) => u32::MAX, Err(_) => u32::MAX,
}; };
let bb = match b { let bb = match b {
OkStd(b) => b as u32, Ok(b) => b as u32,
Err(_) => u32::MAX, Err(_) => u32::MAX,
}; };
log(LogMessage::TestSensor, aa, bb, &plant.to_string(), ""); LOG_ACCESS
.lock()
.await
.log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "")
.await;
} }
self.esp.delay.delay_ms(10); Timer::after_millis(10).await;
anyhow::Ok(()) Ok(())
} }
fn set_config(&mut self, config: PlantControllerConfig) -> anyhow::Result<()> { fn set_config(&mut self, config: PlantControllerConfig) {
self.config = config; self.config = config;
self.esp.save_config(&self.config)?;
anyhow::Ok(())
} }
fn get_mptt_voltage(&mut self) -> anyhow::Result<Voltage> { async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError> {
self.charger.get_mptt_voltage() self.charger.get_mptt_voltage()
} }
fn get_mptt_current(&mut self) -> anyhow::Result<Current> { async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
self.charger.get_mptt_current() self.charger.get_mppt_current()
} }
} }

View File

@@ -1,16 +1,23 @@
use embedded_hal_bus::i2c::MutexDevice; use crate::hal::Box;
use esp_idf_hal::can::CanDriver;
use esp_idf_hal::delay::Delay;
use esp_idf_hal::i2c::I2cDriver;
use esp_idf_hal::pcnt::PcntDriver;
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
use crate::hal::Sensor; use crate::hal::Sensor;
use crate::log::{log, LogMessage}; use crate::log::{LogMessage, LOG_ACCESS};
use crate::fat_error::{FatError, FatResult};
use alloc::format;
use alloc::string::ToString;
use async_trait::async_trait;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::Timer;
use esp_hal::i2c::master::I2c;
use esp_hal::twai::Twai;
use esp_hal::Blocking;
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
const REPEAT_MOIST_MEASURE: usize = 10; const REPEAT_MOIST_MEASURE: usize = 10;
#[async_trait]
pub trait SensorInteraction { pub trait SensorInteraction {
fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> anyhow::Result<f32>; async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> FatResult<f32>;
} }
const MS0: u8 = 1_u8; const MS0: u8 = 1_u8;
@@ -20,24 +27,30 @@ const MS3: u8 = 4_u8;
const MS4: u8 = 2_u8; const MS4: u8 = 2_u8;
const SENSOR_ON: u8 = 5_u8; const SENSOR_ON: u8 = 5_u8;
pub enum SensorImpl<'a> { pub enum SensorImpl {
PulseCounter { PulseCounter {
signal_counter: PcntDriver<'a>, //signal_counter: PcntDriver<'a>,
sensor_expander: Pca9535Immediate<MutexDevice<'a, I2cDriver<'a>>>, sensor_expander:
Pca9535Immediate<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>>,
}, },
CanBus { CanBus {
can: CanDriver<'a> twai: Twai<'static, Blocking>,
} },
} }
impl SensorInteraction for SensorImpl<'_> { #[async_trait]
fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> anyhow::Result<f32> { impl SensorInteraction for SensorImpl {
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> FatResult<f32> {
match self { match self {
SensorImpl::PulseCounter { signal_counter, sensor_expander, .. } => { SensorImpl::PulseCounter {
//signal_counter,
sensor_expander,
..
} => {
let mut results = [0_f32; REPEAT_MOIST_MEASURE]; let mut results = [0_f32; REPEAT_MOIST_MEASURE];
for repeat in 0..REPEAT_MOIST_MEASURE { for repeat in 0..REPEAT_MOIST_MEASURE {
signal_counter.counter_pause()?; //signal_counter.counter_pause()?;
signal_counter.counter_clear()?; //signal_counter.counter_clear()?;
//Disable all //Disable all
sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?; sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?;
@@ -70,45 +83,46 @@ impl SensorInteraction for SensorImpl<'_> {
} }
sensor_expander.pin_set_low(GPIOBank::Bank0, MS4)?; sensor_expander.pin_set_low(GPIOBank::Bank0, MS4)?;
sensor_expander sensor_expander.pin_set_high(GPIOBank::Bank0, SENSOR_ON)?;
.pin_set_high(GPIOBank::Bank0, SENSOR_ON)?;
let delay = Delay::new_default();
let measurement = 100; // TODO what is this scaling factor? what is its purpose? let measurement = 100; // TODO what is this scaling factor? what is its purpose?
let factor = 1000f32 / measurement as f32; let factor = 1000f32 / measurement as f32;
//give some time to stabilize //give some time to stabilize
delay.delay_ms(10); Timer::after_millis(10).await;
signal_counter.counter_resume()?; //signal_counter.counter_resume()?;
delay.delay_ms(measurement); Timer::after_millis(measurement).await;
signal_counter.counter_pause()?; //signal_counter.counter_pause()?;
sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?; sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?;
sensor_expander sensor_expander.pin_set_low(GPIOBank::Bank0, SENSOR_ON)?;
.pin_set_low(GPIOBank::Bank0, SENSOR_ON)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?; sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?; sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?; sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?; sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?;
delay.delay_ms(10); Timer::after_millis(10).await;
let unscaled = signal_counter.get_counter_value()? as i32; let unscaled = 1337; //signal_counter.get_counter_value()? as i32;
let hz = unscaled as f32 * factor; let hz = unscaled as f32 * factor;
log( LOG_ACCESS
.lock()
.await
.log(
LogMessage::RawMeasure, LogMessage::RawMeasure,
unscaled as u32, unscaled as u32,
hz as u32, hz as u32,
&plant.to_string(), &plant.to_string(),
&format!("{sensor:?}"), &format!("{sensor:?}"),
); )
.await;
results[repeat] = hz; results[repeat] = hz;
} }
results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
let mid = results.len() / 2; let mid = results.len() / 2;
let median = results[mid]; let median = results[mid];
anyhow::Ok(median) Ok(median)
} }
SensorImpl::CanBus { .. } => { SensorImpl::CanBus { twai } => {
todo!() Err(FatError::String {error: "Not yet implemented".to_string()})
} }
} }
} }

View File

@@ -1,112 +1,115 @@
use crate::hal::TANK_MULTI_SAMPLE; use crate::bail;
use anyhow::{anyhow, bail}; use crate::fat_error::FatError;
use ds18b20::Ds18b20; use crate::hal::{ADC1, TANK_MULTI_SAMPLE};
use esp_idf_hal::adc::oneshot::config::AdcChannelConfig; use embassy_time::Timer;
use esp_idf_hal::adc::oneshot::{AdcChannelDriver, AdcDriver}; use esp_hal::analog::adc::{Adc, AdcConfig, AdcPin, Attenuation};
use esp_idf_hal::adc::{attenuation, Resolution, ADC1}; use esp_hal::delay::Delay;
use esp_idf_hal::delay::Delay; use esp_hal::gpio::{Flex, Input, Output, OutputConfig, Pull};
use esp_idf_hal::gpio::{AnyIOPin, AnyInputPin, Gpio5, InputOutput, PinDriver, Pull}; use esp_hal::pcnt::unit::Unit;
use esp_idf_hal::pcnt::{ use esp_hal::peripherals::GPIO5;
PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex, PCNT1, use esp_hal::Blocking;
}; use esp_println::println;
use esp_idf_sys::EspError; use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20};
use one_wire_bus::OneWire;
pub struct TankSensor<'a> { pub struct TankSensor<'a> {
one_wire_bus: OneWire<PinDriver<'a, AnyIOPin, InputOutput>>, one_wire_bus: OneWire<Flex<'a>>,
tank_channel: AdcChannelDriver<'a, Gpio5, AdcDriver<'a, ADC1>>, tank_channel: Adc<'a, ADC1<'a>, Blocking>,
tank_power: PinDriver<'a, AnyIOPin, InputOutput>, tank_power: Output<'a>,
flow_counter: PcntDriver<'a>, tank_pin: AdcPin<GPIO5<'a>, ADC1<'a>>,
delay: Delay, // flow_counter: PcntDriver<'a>,
// delay: Delay,
} }
impl<'a> TankSensor<'a> { impl<'a> TankSensor<'a> {
pub(crate) fn create( pub(crate) fn create(
one_wire_pin: AnyIOPin, mut one_wire_pin: Flex<'a>,
adc1: ADC1, adc1: ADC1<'a>,
gpio5: Gpio5, gpio5: GPIO5<'a>,
tank_power_pin: AnyIOPin, tank_power: Output<'a>,
flow_sensor_pin: AnyIOPin, flow_sensor: Input,
pcnt1: PCNT1, pcnt1: Unit<'a, 1>,
) -> anyhow::Result<TankSensor<'a>> { ) -> Result<TankSensor<'a>, FatError> {
let mut one_wire_pin = one_wire_pin.apply_output_config(&OutputConfig::default().with_pull(Pull::None));
PinDriver::input_output_od(one_wire_pin).expect("Failed to configure pin");
one_wire_pin
.set_pull(Pull::Floating)
.expect("Failed to set pull");
let adc_config = AdcChannelConfig { let mut adc1_config = AdcConfig::new();
attenuation: attenuation::DB_11, let tank_pin = adc1_config.enable_pin(gpio5, Attenuation::_11dB);
resolution: Resolution::Resolution12Bit, let tank_channel = Adc::new(adc1, adc1_config);
calibration: esp_idf_hal::adc::oneshot::config::Calibration::Curve,
};
let tank_driver = AdcDriver::new(adc1).expect("Failed to configure ADC");
let tank_channel = AdcChannelDriver::new(tank_driver, gpio5, &adc_config)
.expect("Failed to configure ADC channel");
let mut tank_power = let one_wire_bus = OneWire::new(one_wire_pin, false);
PinDriver::input_output(tank_power_pin).expect("Failed to configure pin");
tank_power
.set_pull(Pull::Floating)
.expect("Failed to set pull");
let one_wire_bus =
OneWire::new(one_wire_pin).expect("OneWire bus did not pull up after release");
let mut flow_counter = PcntDriver::new(
pcnt1,
Some(flow_sensor_pin),
Option::<AnyInputPin>::None,
Option::<AnyInputPin>::None,
Option::<AnyInputPin>::None,
)?;
flow_counter.channel_config(
PcntChannel::Channel1,
PinIndex::Pin0,
PinIndex::Pin1,
&PcntChannelConfig {
lctrl_mode: PcntControlMode::Keep,
hctrl_mode: PcntControlMode::Keep,
pos_mode: PcntCountMode::Increment,
neg_mode: PcntCountMode::Hold,
counter_h_lim: i16::MAX,
counter_l_lim: 0,
},
)?;
//
// let mut flow_counter = PcntDriver::new(
// pcnt1,
// Some(flow_sensor_pin),
// Option::<AnyInputPin>::None,
// Option::<AnyInputPin>::None,
// Option::<AnyInputPin>::None,
// )?;
//
// flow_counter.channel_config(
// PcntChannel::Channel1,
// PinIndex::Pin0,
// PinIndex::Pin1,
// &PcntChannelConfig {
// lctrl_mode: PcntControlMode::Keep,
// hctrl_mode: PcntControlMode::Keep,
// pos_mode: PcntCountMode::Increment,
// neg_mode: PcntCountMode::Hold,
// counter_h_lim: i16::MAX,
// counter_l_lim: 0,
// },
// )?;
//
Ok(TankSensor { Ok(TankSensor {
one_wire_bus, one_wire_bus,
tank_channel, tank_channel,
tank_power, tank_power,
flow_counter, tank_pin, // flow_counter,
delay: Default::default(), // delay: Default::default(),
}) })
} }
pub fn reset_flow_meter(&mut self) { pub fn reset_flow_meter(&mut self) {
self.flow_counter.counter_pause().unwrap(); // self.flow_counter.counter_pause().unwrap();
self.flow_counter.counter_clear().unwrap(); // self.flow_counter.counter_clear().unwrap();
} }
pub fn start_flow_meter(&mut self) { pub fn start_flow_meter(&mut self) {
self.flow_counter.counter_resume().unwrap(); //self.flow_counter.counter_resume().unwrap();
} }
pub fn get_flow_meter_value(&mut self) -> i16 { pub fn get_flow_meter_value(&mut self) -> i16 {
self.flow_counter.get_counter_value().unwrap() //self.flow_counter.get_counter_value().unwrap()
5_i16
} }
pub fn stop_flow_meter(&mut self) -> i16 { pub fn stop_flow_meter(&mut self) -> i16 {
self.flow_counter.counter_pause().unwrap(); //self.flow_counter.counter_pause().unwrap();
self.get_flow_meter_value() self.get_flow_meter_value()
} }
pub fn water_temperature_c(&mut self) -> anyhow::Result<f32> { pub async fn water_temperature_c(&mut self) -> Result<f32, FatError> {
//multisample should be moved to water_temperature_c //multisample should be moved to water_temperature_c
let mut attempt = 1; let mut attempt = 1;
let water_temp: Result<f32, anyhow::Error> = loop { let mut delay = Delay::new();
let temp = self.single_temperature_c(); self.one_wire_bus.reset(&mut delay)?;
let mut search = DeviceSearch::new();
let mut water_temp_sensor: Option<Device> = None;
while let Some(device) = self.one_wire_bus.search_next(&mut search, &mut delay)? {
if device.address[0] == ds18b20::FAMILY_CODE {
water_temp_sensor = Some(device);
break;
}
}
match water_temp_sensor {
Some(device) => {
println!("Found one wire device: {:?}", device);
let mut water_temp_sensor = DS18B20::new(device)?;
let water_temp: Result<f32, FatError> = loop {
let temp = self
.single_temperature_c(&mut water_temp_sensor, &mut delay)
.await;
match &temp { match &temp {
Ok(res) => { Ok(res) => {
println!("Water temp is {}", res); println!("Water temp is {}", res);
@@ -123,49 +126,43 @@ impl<'a> TankSensor<'a> {
}; };
water_temp water_temp
} }
None => {
fn single_temperature_c(&mut self) -> anyhow::Result<f32> {
self.one_wire_bus
.reset(&mut self.delay)
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
let first = self.one_wire_bus.devices(false, &mut self.delay).next();
if first.is_none() {
bail!("Not found any one wire Ds18b20"); bail!("Not found any one wire Ds18b20");
} }
let device_address = first }
.unwrap() }
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
let water_temp_sensor = Ds18b20::new::<EspError>(device_address) async fn single_temperature_c(
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; &mut self,
sensor: &mut DS18B20,
water_temp_sensor delay: &mut Delay,
.start_temp_measurement(&mut self.one_wire_bus, &mut self.delay) ) -> Result<f32, FatError> {
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?; let resolution = sensor.measure_temperature(&mut self.one_wire_bus, delay)?;
ds18b20::Resolution::Bits12.delay_for_measurement_time(&mut self.delay); Timer::after_millis(resolution.time_ms() as u64).await;
let sensor_data = water_temp_sensor let temperature = sensor.read_temperature(&mut self.one_wire_bus, delay)? as f32;
.read_data(&mut self.one_wire_bus, &mut self.delay) if temperature == 85_f32 {
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
if sensor_data.temperature == 85_f32 {
bail!("Ds18b20 dummy temperature returned"); bail!("Ds18b20 dummy temperature returned");
} }
anyhow::Ok(sensor_data.temperature / 10_f32) Ok(temperature / 10_f32)
} }
pub fn tank_sensor_voltage(&mut self) -> anyhow::Result<f32> { pub async fn tank_sensor_voltage(&mut self) -> Result<f32, FatError> {
self.tank_power.set_high()?; self.tank_power.set_high();
//let stabilize //let stabilize
self.delay.delay_ms(100); Timer::after_millis(100).await;
let mut store = [0_u16; TANK_MULTI_SAMPLE]; let mut store = [0_u16; TANK_MULTI_SAMPLE];
for multisample in 0..TANK_MULTI_SAMPLE { for multisample in 0..TANK_MULTI_SAMPLE {
let value = self.tank_channel.read()?; let value = self.tank_channel.read_oneshot(&mut self.tank_pin);
store[multisample] = value; //force yield
Timer::after_millis(10).await;
store[multisample] = value.unwrap();
} }
self.tank_power.set_low()?; self.tank_power.set_low();
store.sort(); store.sort();
let median_mv = store[6] as f32 / 1000_f32; //TODO probably wrong? check!
anyhow::Ok(median_mv) let median_mv = store[6] as f32 * 3300_f32 / 4096_f32;
Ok(median_mv)
} }
} }

View File

@@ -1,4 +0,0 @@
#![allow(dead_code)]
extern crate embedded_hal as hal;
pub mod sipo;

View File

@@ -1,42 +1,145 @@
use crate::hal::TIME_ACCESS;
use crate::vec;
use alloc::string::ToString;
use alloc::vec::Vec;
use bytemuck::{AnyBitPattern, Pod, Zeroable};
use deranged::RangedU8;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::Mutex;
use esp_hal::Persistable;
use log::info;
use serde::Serialize; use serde::Serialize;
use std::{collections::HashMap, sync::Mutex};
use strum::EnumIter;
use strum_macros::IntoStaticStr; use strum_macros::IntoStaticStr;
use esp_idf_svc::systime::EspSystemTime;
use once_cell::sync::Lazy;
use ringbuffer::{ConstGenericRingBuffer, RingBuffer};
use text_template::Template;
use unit_enum::UnitEnum; use unit_enum::UnitEnum;
const LOG_ARRAY_SIZE: u8 = 220;
const MAX_LOG_ARRAY_INDEX: u8 = LOG_ARRAY_SIZE - 1;
#[esp_hal::ram(rtc_fast, persistent)]
static mut LOG_ARRAY: LogArray = LogArray {
buffer: [LogEntryInner {
timestamp: 0,
message_id: 0,
a: 0,
b: 0,
txt_short: [0; TXT_SHORT_LENGTH],
txt_long: [0; TXT_LONG_LENGTH],
}; LOG_ARRAY_SIZE as usize],
head: 0,
};
pub static LOG_ACCESS: Mutex<CriticalSectionRawMutex, &'static mut LogArray> =
unsafe { Mutex::new(&mut *&raw mut LOG_ARRAY) };
const TXT_SHORT_LENGTH: usize = 8; const TXT_SHORT_LENGTH: usize = 8;
const TXT_LONG_LENGTH: usize = 32; const TXT_LONG_LENGTH: usize = 32;
const BUFFER_SIZE: usize = 220; #[derive(Debug, Clone, Copy, AnyBitPattern)]
#[repr(C)]
pub struct LogArray {
buffer: [LogEntryInner; LOG_ARRAY_SIZE as usize],
head: u8,
}
#[link_section = ".rtc.data"] unsafe impl Persistable for LogArray {}
static mut BUFFER: ConstGenericRingBuffer<LogEntry, BUFFER_SIZE> = unsafe impl Zeroable for LogEntryInner {}
ConstGenericRingBuffer::<LogEntry, BUFFER_SIZE>::new();
#[allow(static_mut_refs)]
static BUFFER_ACCESS: Lazy<Mutex<&mut ConstGenericRingBuffer<LogEntry, BUFFER_SIZE>>> =
Lazy::new(|| unsafe { Mutex::new(&mut BUFFER) });
#[derive(Serialize, Debug, Clone)] unsafe impl Pod for LogEntryInner {}
#[derive(Debug, Clone, Copy)]
struct LogEntryInner {
pub timestamp: u64,
pub message_id: u16,
pub a: u32,
pub b: u32,
pub txt_short: [u8; TXT_SHORT_LENGTH],
pub txt_long: [u8; TXT_LONG_LENGTH],
}
#[derive(Serialize)]
pub struct LogEntry { pub struct LogEntry {
pub timestamp: u64, pub timestamp: u64,
pub message_id: u16, pub message_id: u16,
pub a: u32, pub a: u32,
pub b: u32, pub b: u32,
pub txt_short: heapless::String<TXT_SHORT_LENGTH>, pub txt_short: alloc::string::String,
pub txt_long: heapless::String<TXT_LONG_LENGTH>, pub txt_long: alloc::string::String,
} }
pub fn init() { impl From<LogEntryInner> for LogEntry {
unsafe { fn from(value: LogEntryInner) -> Self {
BUFFER = ConstGenericRingBuffer::<LogEntry, BUFFER_SIZE>::new(); LogEntry {
}; timestamp: value.timestamp,
let mut access = BUFFER_ACCESS.lock().unwrap(); message_id: value.message_id,
access.drain().for_each(|_| {}); a: value.a,
b: value.b,
txt_short: alloc::string::String::from_utf8_lossy_owned(value.txt_short.to_vec()),
txt_long: alloc::string::String::from_utf8_lossy_owned(value.txt_long.to_vec()),
}
}
}
impl LogArray {
pub fn get(&mut self) -> Vec<LogEntry> {
let head: RangedU8<0, MAX_LOG_ARRAY_INDEX> =
RangedU8::new(self.head).unwrap_or(RangedU8::new(0).unwrap());
let mut rv: Vec<LogEntry> = Vec::new();
let mut index = head.wrapping_sub(1);
for _ in 0..self.buffer.len() {
let entry = self.buffer[index.get() as usize];
if (entry.message_id as usize) != LogMessage::Empty.ordinal() {
rv.push(entry.into());
}
index = index.wrapping_sub(1);
}
rv
}
pub async fn log(
&mut self,
message_key: LogMessage,
number_a: u32,
number_b: u32,
txt_short: &str,
txt_long: &str,
) {
let mut head: RangedU8<0, MAX_LOG_ARRAY_INDEX> =
RangedU8::new(self.head).unwrap_or(RangedU8::new(0).unwrap());
let mut txt_short_stack: heapless::String<TXT_SHORT_LENGTH> = heapless::String::new();
let mut txt_long_stack: heapless::String<TXT_LONG_LENGTH> = heapless::String::new();
limit_length(txt_short, &mut txt_short_stack);
limit_length(txt_long, &mut txt_long_stack);
let time = {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} / 1000;
let ordinal = message_key.ordinal() as u16;
let template: &str = message_key.into();
let mut template_string = template.to_string();
template_string = template_string.replace("${number_a}", number_a.to_string().as_str());
template_string = template_string.replace("${number_b}", number_b.to_string().as_str());
template_string = template_string.replace("${txt_long}", txt_long);
template_string = template_string.replace("${txt_short}", txt_short);
info!("{}", template_string);
let to_modify = &mut self.buffer[head.get() as usize];
to_modify.timestamp = time;
to_modify.message_id = ordinal;
to_modify.a = number_a;
to_modify.b = number_b;
to_modify
.txt_short
.clone_from_slice(&txt_short_stack.as_bytes());
to_modify
.txt_long
.clone_from_slice(&txt_long_stack.as_bytes());
head = head.wrapping_add(1);
self.head = head.get();
}
} }
fn limit_length<const LIMIT: usize>(input: &str, target: &mut heapless::String<LIMIT>) { fn limit_length<const LIMIT: usize>(input: &str, target: &mut heapless::String<LIMIT>) {
@@ -55,79 +158,15 @@ fn limit_length<const LIMIT: usize>(input: &str, target: &mut heapless::String<L
} }
} }
} }
} while target.len() < LIMIT {
target.push(' ').unwrap();
pub fn get_log() -> Vec<LogEntry> {
let buffer = BUFFER_ACCESS.lock().unwrap();
let mut read_copy = Vec::new();
for entry in buffer.iter() {
let copy = entry.clone();
read_copy.push(copy);
}
drop(buffer);
read_copy
}
pub fn log(message_key: LogMessage, number_a: u32, number_b: u32, txt_short: &str, txt_long: &str) {
let mut txt_short_stack: heapless::String<TXT_SHORT_LENGTH> = heapless::String::new();
let mut txt_long_stack: heapless::String<TXT_LONG_LENGTH> = heapless::String::new();
limit_length(txt_short, &mut txt_short_stack);
limit_length(txt_long, &mut txt_long_stack);
let time = EspSystemTime {}.now().as_millis() as u64;
let ordinal = message_key.ordinal() as u16;
let template_string: &str = message_key.into();
let mut values: HashMap<&str, &str> = HashMap::new();
let number_a_str = number_a.to_string();
let number_b_str = number_b.to_string();
values.insert("number_a", &number_a_str);
values.insert("number_b", &number_b_str);
values.insert("txt_short", txt_short);
values.insert("txt_long", txt_long);
let template = Template::from(template_string);
let serial_entry = template.fill_in(&values);
println!("{serial_entry}");
//TODO push to mqtt?
let entry = LogEntry {
timestamp: time,
message_id: ordinal,
a: number_a,
b: number_b,
txt_short: txt_short_stack,
txt_long: txt_long_stack,
};
let mut buffer = BUFFER_ACCESS.lock().unwrap();
buffer.push(entry);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn within_limit() {
let test = "12345678";
let mut txt_short_stack: heapless::String<TXT_SHORT_LENGTH> = heapless::String::new();
let mut txt_long_stack: heapless::String<TXT_LONG_LENGTH> = heapless::String::new();
limit_length(test, &mut txt_short_stack);
limit_length(test, &mut txt_long_stack);
assert_eq!(txt_short_stack.as_str(), test);
assert_eq!(txt_long_stack.as_str(), test);
} }
} }
#[derive(IntoStaticStr, EnumIter, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, UnitEnum)] #[derive(IntoStaticStr, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, UnitEnum)]
pub enum LogMessage { pub enum LogMessage {
#[strum(serialize = "")]
Empty,
#[strum( #[strum(
serialize = "Reset due to ${txt_long} requires rtc clear ${number_a} and force config mode ${number_b}" serialize = "Reset due to ${txt_long} requires rtc clear ${number_a} and force config mode ${number_b}"
)] )]
@@ -160,7 +199,7 @@ pub enum LogMessage {
StayAlive, StayAlive,
#[strum(serialize = "Connecting mqtt ${txt_short} with id ${txt_long}")] #[strum(serialize = "Connecting mqtt ${txt_short} with id ${txt_long}")]
MqttInfo, MqttInfo,
#[strum(serialize = "Received stay alive with value ${txt_short}")] #[strum(serialize = "Received stay alive with value ${number_a}")]
MqttStayAliveRec, MqttStayAliveRec,
#[strum(serialize = "Unknown topic recieved ${txt_long}")] #[strum(serialize = "Unknown topic recieved ${txt_long}")]
UnknownTopic, UnknownTopic,

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ use crate::{
hal::{Sensor, HAL}, hal::{Sensor, HAL},
in_time_range, in_time_range,
}; };
use alloc::string::{String, ToString};
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -115,9 +116,13 @@ fn map_range_moisture(
} }
impl PlantState { impl PlantState {
pub fn read_hardware_state(plant_id: usize, board: &mut HAL) -> Self { pub async fn read_hardware_state(plant_id: usize, board: &mut HAL<'_>) -> Self {
let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a { let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a {
match board.board_hal.measure_moisture_hz(plant_id, Sensor::A) { match board
.board_hal
.measure_moisture_hz(plant_id, Sensor::A)
.await
{
Ok(raw) => match map_range_moisture( Ok(raw) => match map_range_moisture(
raw, raw,
board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency, board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency,
@@ -138,7 +143,11 @@ impl PlantState {
}; };
let sensor_b = if board.board_hal.get_config().plants[plant_id].sensor_b { let sensor_b = if board.board_hal.get_config().plants[plant_id].sensor_b {
match board.board_hal.measure_moisture_hz(plant_id, Sensor::B) { match board
.board_hal
.measure_moisture_hz(plant_id, Sensor::B)
.await
{
Ok(raw) => match map_range_moisture( Ok(raw) => match map_range_moisture(
raw, raw,
board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency, board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency,

View File

@@ -1,5 +1,9 @@
use crate::{config::TankConfig, hal::HAL}; use crate::alloc::string::{String, ToString};
use anyhow::Context; use crate::config::TankConfig;
use crate::hal::HAL;
use crate::fat_error::FatResult;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::MutexGuard;
use serde::Serialize; use serde::Serialize;
const OPEN_TANK_VOLTAGE: f32 = 3.0; const OPEN_TANK_VOLTAGE: f32 = 3.0;
@@ -113,7 +117,7 @@ impl TankState {
} }
} }
pub fn as_mqtt_info(&self, config: &TankConfig, water_temp: &anyhow::Result<f32>) -> TankInfo { pub fn as_mqtt_info(&self, config: &TankConfig, water_temp: &FatResult<f32>) -> TankInfo {
let mut tank_err: Option<TankError> = None; let mut tank_err: Option<TankError> = None;
let left_ml = match self.left_ml(config) { let left_ml = match self.left_ml(config) {
Err(err) => { Err(err) => {
@@ -150,15 +154,16 @@ impl TankState {
} }
} }
pub fn determine_tank_state(board: &mut std::sync::MutexGuard<'_, HAL<'_>>) -> TankState { pub async fn determine_tank_state(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
) -> TankState {
if board.board_hal.get_config().tank.tank_sensor_enabled { if board.board_hal.get_config().tank.tank_sensor_enabled {
match board match board
.board_hal .board_hal
.get_tank_sensor() .get_tank_sensor()
.context("no sensor") .and_then(|f| core::prelude::v1::Ok(f.tank_sensor_voltage()))
.and_then(|f| f.tank_sensor_voltage())
{ {
Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv), Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv.await.unwrap()),
Err(err) => TankState::Error(TankError::BoardError(err.to_string())), Err(err) => TankState::Error(TankError::BoardError(err.to_string())),
} }
} else { } else {
@@ -170,20 +175,20 @@ pub fn determine_tank_state(board: &mut std::sync::MutexGuard<'_, HAL<'_>>) -> T
/// Information structure send to mqtt for monitoring purposes /// Information structure send to mqtt for monitoring purposes
pub struct TankInfo { pub struct TankInfo {
/// there is enough water in the tank /// there is enough water in the tank
enough_water: bool, pub(crate) enough_water: bool,
/// warning that water needs to be refilled soon /// warning that water needs to be refilled soon
warn_level: bool, pub(crate) warn_level: bool,
/// estimation how many ml are still in the tank /// estimation how many ml are still in the tank
left_ml: Option<f32>, pub(crate) left_ml: Option<f32>,
/// if there is an issue with the water level sensor /// if there is an issue with the water level sensor
sensor_error: Option<TankError>, pub(crate) sensor_error: Option<TankError>,
/// raw water sensor value /// raw water sensor value
raw: Option<f32>, pub(crate) raw: Option<f32>,
/// percent value /// percent value
percent: Option<f32>, pub(crate) percent: Option<f32>,
/// water in the tank might be frozen /// water in the tank might be frozen
water_frozen: bool, pub(crate) water_frozen: bool,
/// water temperature /// water temperature
water_temp: Option<f32>, pub(crate) water_temp: Option<f32>,
temp_sensor_error: Option<String>, pub(crate) temp_sensor_error: Option<String>,
} }

View File

@@ -0,0 +1,191 @@
use crate::fat_error::{FatError, FatResult};
use crate::hal::rtc::X25;
use crate::BOARD_ACCESS;
use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::{String, ToString};
use chrono::DateTime;
use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write};
use log::info;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct WebBackupHeader {
timestamp: String,
size: u16,
}
pub(crate) async fn get_backup_config<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
) -> FatResult<Option<u32>>
where
T: Read + Write,
{
// First pass: verify checksum without sending data
let mut checksum = X25.digest();
let mut chunk = 0_usize;
loop {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk as u32).await;
let (buf, len, expected_crc) = board
.board_hal
.get_rtc_module()
.get_backup_config(chunk)
.await?;
// Update checksum with the actual data bytes of this chunk
checksum.update(&buf[..len]);
let is_last = len == 0 || len < buf.len();
if is_last {
let actual_crc = checksum.finalize();
if actual_crc != expected_crc {
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
conn.initiate_response(
409,
Some(
format!(
"Checksum mismatch expected {} got {}",
expected_crc, actual_crc
)
.as_str(),
),
&[],
)
.await?;
return Ok(Some(409));
}
break;
}
chunk += 1;
}
// Second pass: stream data
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
let mut chunk = 0_usize;
loop {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk as u32).await;
let (buf, len, _expected_crc) = board
.board_hal
.get_rtc_module()
.get_backup_config(chunk)
.await?;
if len == 0 {
break;
}
conn.write_all(&buf[..len]).await?;
if len < buf.len() {
break;
}
chunk += 1;
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
Ok(Some(200))
}
pub(crate) async fn backup_config<T, const N: usize>(
conn: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>>
where
T: Read + Write,
{
let mut offset = 0_usize;
let mut buf = [0_u8; 32];
let mut checksum = X25.digest();
let mut counter = 0;
loop {
let to_write = conn.read(&mut buf).await?;
if to_write == 0 {
info!("backup finished");
break;
} else {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(counter).await;
counter = counter + 1;
board
.board_hal
.get_rtc_module()
.backup_config(offset, &buf[0..to_write])
.await?;
checksum.update(&buf[0..to_write]);
}
offset = offset + to_write;
}
let mut board = BOARD_ACCESS.get().await.lock().await;
board
.board_hal
.get_rtc_module()
.backup_config_finalize(checksum.finalize(), offset)
.await?;
board.board_hal.clear_progress().await;
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Ok(Some("saved".to_owned()))
}
pub(crate) async fn backup_info<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> Result<Option<String>, FatError>
where
T: Read + Write,
{
let mut board = BOARD_ACCESS.get().await.lock().await;
let header = board.board_hal.get_rtc_module().get_backup_info().await;
let json = match header {
Ok(h) => {
let timestamp = DateTime::from_timestamp_millis(h.timestamp).unwrap();
let wbh = WebBackupHeader {
timestamp: timestamp.to_rfc3339(),
size: h.size,
};
serde_json::to_string(&wbh)?
}
Err(err) => {
let wbh = WebBackupHeader {
timestamp: err.to_string(),
size: 0,
};
serde_json::to_string(&wbh)?
}
};
Ok(Some(json))
}

View File

@@ -0,0 +1,160 @@
use crate::fat_error::{FatError, FatResult};
use crate::BOARD_ACCESS;
use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::String;
use edge_http::io::server::Connection;
use edge_http::Method;
use embedded_io_async::{Read, Write};
use log::info;
pub(crate) async fn list_files<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let result = board.board_hal.get_esp().list_files().await?;
let file_list_json = serde_json::to_string(&result)?;
Ok(Some(file_list_json))
}
pub(crate) async fn file_operations<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
method: Method,
path: &&str,
prefix: &&str,
) -> Result<Option<u32>, FatError>
where
T: Read + Write,
{
let filename = &path[prefix.len()..];
info!("file request for {} with method {}", filename, method);
Ok(match method {
Method::Delete => {
let mut board = BOARD_ACCESS.get().await.lock().await;
board
.board_hal
.get_esp()
.delete_file(filename.to_owned())
.await?;
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Some(200)
}
Method::Get => {
let disposition = format!("attachment; filename=\"{filename}\"");
let size = {
let mut board = BOARD_ACCESS.get().await.lock().await;
board
.board_hal
.get_esp()
.get_size(filename.to_owned())
.await?
};
conn.initiate_response(
200,
Some("OK"),
&[
("Content-Type", "application/octet-stream"),
("Content-Disposition", disposition.as_str()),
("Content-Length", &format!("{}", size)),
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
let mut chunk = 0;
loop {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk).await;
let read_chunk = board
.board_hal
.get_esp()
.get_file(filename.to_owned(), chunk)
.await?;
let length = read_chunk.1;
if length == 0 {
info!("file request for {} finished", filename);
break;
}
let data = &read_chunk.0[0..length];
conn.write_all(data).await?;
if length < read_chunk.0.len() {
info!("file request for {} finished", filename);
break;
}
chunk = chunk + 1;
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
Some(200)
}
Method::Post => {
{
let mut board = BOARD_ACCESS.get().await.lock().await;
//ensure the file is deleted first; otherwise we would need to truncate the file which will not work with streaming
let _ = board
.board_hal
.get_esp()
.delete_file(filename.to_owned())
.await;
}
let mut offset = 0_usize;
let mut chunk = 0;
loop {
let mut buf = [0_u8; 1024];
let to_write = conn.read(&mut buf).await?;
if to_write == 0 {
info!("file request for {} finished", filename);
break;
} else {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk as u32).await;
board
.board_hal
.get_esp()
.write_file(filename.to_owned(), offset as u32, &buf[0..to_write])
.await?;
}
offset = offset + to_write;
chunk = chunk + 1;
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Some(200)
}
_ => None,
})
}

View File

@@ -0,0 +1,167 @@
use crate::fat_error::{FatError, FatResult};
use crate::hal::{esp_time, PLANT_COUNT};
use crate::log::LogMessage;
use crate::plant_state::{MoistureSensorState, PlantState};
use crate::tank::determine_tank_state;
use crate::{get_version, BOARD_ACCESS};
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use chrono_tz::Tz;
use core::str::FromStr;
use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write};
use serde::Serialize;
#[derive(Serialize, Debug)]
struct LoadData<'a> {
rtc: &'a str,
native: &'a str,
}
#[derive(Serialize, Debug)]
struct Moistures {
moisture_a: Vec<String>,
moisture_b: Vec<String>,
}
#[derive(Serialize, Debug)]
struct SolarState {
mppt_voltage: f32,
mppt_current: f32,
is_day: bool,
}
pub(crate) async fn get_live_moisture<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>>
where
T: Read + Write,
{
let mut board = BOARD_ACCESS.get().await.lock().await;
let mut plant_state = Vec::new();
for i in 0..PLANT_COUNT {
plant_state.push(PlantState::read_hardware_state(i, &mut board).await);
}
let a = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_a {
MoistureSensorState::Disabled => "disabled".to_string(),
MoistureSensorState::MoistureValue {
raw_hz,
moisture_percent,
} => {
format!("{moisture_percent:.2}% {raw_hz}hz",)
}
MoistureSensorState::SensorError(err) => format!("{err:?}"),
}));
let b = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_b {
MoistureSensorState::Disabled => "disabled".to_string(),
MoistureSensorState::MoistureValue {
raw_hz,
moisture_percent,
} => {
format!("{moisture_percent:.2}% {raw_hz}hz",)
}
MoistureSensorState::SensorError(err) => format!("{err:?}"),
}));
let data = Moistures {
moisture_a: a,
moisture_b: b,
};
let json = serde_json::to_string(&data)?;
Ok(Some(json))
}
pub(crate) async fn tank_info<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> Result<Option<String>, FatError>
where
T: Read + Write,
{
let mut board = BOARD_ACCESS.get().await.lock().await;
let tank_state = determine_tank_state(&mut board).await;
//should be multisampled
let sensor = board.board_hal.get_tank_sensor()?;
let water_temp: FatResult<f32> = sensor.water_temperature_c().await;
Ok(Some(serde_json::to_string(&tank_state.as_mqtt_info(
&board.board_hal.get_config().tank,
&water_temp,
))?))
}
pub(crate) async fn get_timezones() -> FatResult<Option<String>> {
// Get all timezones compiled into the binary from chrono-tz
let timezones: Vec<&'static str> = chrono_tz::TZ_VARIANTS.iter().map(|tz| tz.name()).collect();
let json = serde_json::to_string(&timezones)?;
Ok(Some(json))
}
pub(crate) async fn get_solar_state<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let state = SolarState {
mppt_voltage: board.board_hal.get_mptt_voltage().await?.as_millivolts() as f32,
mppt_current: board.board_hal.get_mptt_current().await?.as_milliamperes() as f32,
is_day: board.board_hal.is_day(),
};
Ok(Some(serde_json::to_string(&state)?))
}
pub(crate) async fn get_version_web<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
Ok(Some(serde_json::to_string(&get_version(&mut board).await)?))
}
pub(crate) async fn get_config<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let json = serde_json::to_string(&board.board_hal.get_config())?;
Ok(Some(json))
}
pub(crate) async fn get_battery_state<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let battery_state = board
.board_hal
.get_battery_monitor()
.get_battery_state()
.await?;
Ok(Some(serde_json::to_string(&battery_state)?))
}
pub(crate) async fn get_time<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let conf = board.board_hal.get_config();
let tz = Tz::from_str(conf.timezone.as_ref().unwrap().as_str()).unwrap();
let native = esp_time().await.with_timezone(&tz).to_rfc3339();
let rtc = match board.board_hal.get_rtc_module().get_rtc_time().await {
Ok(time) => time.with_timezone(&tz).to_rfc3339(),
Err(err) => {
format!("Error getting time: {}", err)
}
};
let data = LoadData {
rtc: rtc.as_str(),
native: native.as_str(),
};
let json = serde_json::to_string(&data)?;
Ok(Some(json))
}
pub(crate) async fn get_log_localization_config<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
Ok(Some(serde_json::to_string(
&LogMessage::to_log_localisation_config(),
)?))
}

View File

@@ -0,0 +1,36 @@
use crate::fat_error::FatResult;
use crate::log::LOG_ACCESS;
use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write};
pub(crate) async fn get_log<T, const N: usize>(
conn: &mut Connection<'_, T, N>,
) -> FatResult<Option<u32>>
where
T: Read + Write,
{
let log = LOG_ACCESS.lock().await.get();
conn.initiate_response(
200,
Some("OK"),
&[
("Content-Type", "text/javascript"),
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
conn.write_all("[".as_bytes()).await?;
let mut append = false;
for entry in log {
if append {
conn.write_all(",".as_bytes()).await?;
}
append = true;
let json = serde_json::to_string(&entry)?;
conn.write_all(json.as_bytes()).await?;
}
conn.write_all("]".as_bytes()).await?;
Ok(Some(200))
}

View File

@@ -0,0 +1,50 @@
use crate::fat_error::FatError;
use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write};
pub(crate) async fn serve_favicon<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
) -> Result<Option<u32>, FatError>
where
T: Read + Write,
{
conn.initiate_response(200, Some("OK"), &[("Content-Type", "image/x-icon")])
.await?;
conn.write_all(include_bytes!("favicon.ico")).await?;
Ok(Some(200))
}
pub(crate) async fn serve_index<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
) -> Result<Option<u32>, FatError>
where
T: Read + Write,
{
conn.initiate_response(
200,
Some("OK"),
&[("Content-Type", "text/html"), ("Content-Encoding", "gzip")],
)
.await?;
conn.write_all(include_bytes!("index.html.gz")).await?;
Ok(Some(200))
}
pub(crate) async fn serve_bundle<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
) -> Result<Option<u32>, FatError>
where
T: Read + Write,
{
conn.initiate_response(
200,
Some("OK"),
&[
("Content-Type", "text/javascript"),
("Content-Encoding", "gzip"),
],
)
.await?;
conn.write_all(include_bytes!("bundle.js.gz")).await?;
Ok(Some(200))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
use crate::config::PlantControllerConfig;
use crate::fat_error::FatResult;
use crate::hal::esp_set_time;
use crate::webserver::read_up_to_bytes_from_request;
use crate::{do_secure_pump, BOARD_ACCESS};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use chrono::DateTime;
use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write};
use log::info;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct NightLampCommand {
active: bool,
}
#[derive(Serialize, Debug)]
struct SSIDList {
ssids: Vec<String>,
}
#[derive(Deserialize, Debug)]
struct SetTime<'a> {
time: &'a str,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct TestPump {
pump: usize,
}
pub(crate) async fn wifi_scan<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
info!("start wifi scan");
let mut ssids: Vec<String> = Vec::new();
let scan_result = board.board_hal.get_esp().wifi_scan().await?;
scan_result
.iter()
.for_each(|s| ssids.push(s.ssid.to_string()));
let ssid_json = serde_json::to_string(&SSIDList { ssids })?;
info!("Sending ssid list {}", &ssid_json);
Ok(Some(ssid_json))
}
pub(crate) async fn board_test() -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.test().await?;
Ok(None)
}
pub(crate) async fn pump_test<T, const N: usize>(
request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>>
where
T: Read + Write,
{
let actual_data = read_up_to_bytes_from_request(request, None).await?;
let pump_test: TestPump = serde_json::from_slice(&actual_data)?;
let mut board = BOARD_ACCESS.get().await.lock().await;
let config = &board.board_hal.get_config().plants[pump_test.pump].clone();
let pump_result = do_secure_pump(&mut board, pump_test.pump, config, false).await;
//ensure it is disabled before unwrapping
board.board_hal.pump(pump_test.pump, false).await?;
Ok(Some(serde_json::to_string(&pump_result?)?))
}
pub(crate) async fn night_lamp_test<T, const N: usize>(
request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>>
where
T: Read + Write,
{
let actual_data = read_up_to_bytes_from_request(request, None).await?;
let light_command: NightLampCommand = serde_json::from_slice(&actual_data)?;
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.light(light_command.active).await?;
Ok(None)
}
pub(crate) async fn write_time<T, const N: usize>(
request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>>
where
T: Read + Write,
{
let actual_data = read_up_to_bytes_from_request(request, None).await?;
let time: SetTime = serde_json::from_slice(&actual_data)?;
let parsed = DateTime::parse_from_rfc3339(time.time).unwrap();
esp_set_time(parsed).await?;
Ok(None)
}
pub(crate) async fn set_config<T, const N: usize>(
request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>>
where
T: Read + Write,
{
let all = read_up_to_bytes_from_request(request, Some(4096)).await?;
let length = all.len();
let config: PlantControllerConfig = serde_json::from_slice(&all)?;
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.get_esp().save_config(all).await?;
info!("Wrote config config {:?} with size {}", config, length);
board.board_hal.set_config(config);
Ok(Some("saved".to_string()))
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
{ {
"devDependencies": { "devDependencies": {
"compression-webpack-plugin": "^11.1.0",
"html-webpack-harddisk-plugin": "^2.0.0", "html-webpack-harddisk-plugin": "^2.0.0",
"html-webpack-plugin": "^5.6.3", "html-webpack-plugin": "^5.6.3",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",

View File

@@ -69,45 +69,44 @@ export class Controller {
}); });
} }
getBackupInfo(): Promise<void> { async getBackupInfo(): Promise<void> {
return fetch(PUBLIC_URL + "/backup_info") try {
.then(response => response.json()) const response = await fetch(PUBLIC_URL + "/backup_info");
.then(json => json as BackupHeader) const json = await response.json();
.then(header => { const header = json as BackupHeader;
controller.submitView.setBackupInfo(header) controller.submitView.setBackupInfo(header);
}) } catch (error) {
.catch(error => {
console.log(error); console.log(error);
}); }
} }
populateTimezones(): Promise<void> { async populateTimezones(): Promise<void> {
return fetch(PUBLIC_URL + '/timezones') try {
.then(response => response.json()) const response = await fetch(PUBLIC_URL + '/timezones');
.then(json => json as string[]) const json = await response.json();
.then(timezones => { const timezones = json as string[];
controller.timeView.timezones(timezones) controller.timeView.timezones(timezones);
}) } catch (error) {
.catch(error => console.error('Error fetching timezones:', error)); return console.error('Error fetching timezones:', error);
}
} }
updateFileList(): Promise<void> { async updateFileList(): Promise<void> {
return fetch(PUBLIC_URL + "/files") try {
.then(response => response.json()) const response = await fetch(PUBLIC_URL + "/files");
.then(json => json as FileList) const json = await response.json();
.then(filelist => { const filelist = json as FileList;
controller.fileview.setFileList(filelist, PUBLIC_URL) controller.fileview.setFileList(filelist, PUBLIC_URL);
}) } catch (error) {
.catch(error => {
console.log(error); console.log(error);
}); }
} }
uploadFile(file: File, name: string) { uploadFile(file: File, name: string) {
var current = 0; let current = 0;
var max = 100; let max = 100;
controller.progressview.addProgress("file_upload", (current / max) * 100, "Uploading File " + name + "(" + current + "/" + max + ")") controller.progressview.addProgress("file_upload", (current / max) * 100, "Uploading File " + name + "(" + current + "/" + max + ")")
var ajax = new XMLHttpRequest(); const ajax = new XMLHttpRequest();
ajax.upload.addEventListener("progress", event => { ajax.upload.addEventListener("progress", event => {
current = event.loaded / 1000; current = event.loaded / 1000;
max = event.total / 1000; max = event.total / 1000;
@@ -133,7 +132,7 @@ export class Controller {
deleteFile(name: string) { deleteFile(name: string) {
controller.progressview.addIndeterminate("file_delete", "Deleting " + name); controller.progressview.addIndeterminate("file_delete", "Deleting " + name);
var ajax = new XMLHttpRequest(); const ajax = new XMLHttpRequest();
ajax.open("DELETE", PUBLIC_URL + "/file?filename=" + name); ajax.open("DELETE", PUBLIC_URL + "/file?filename=" + name);
ajax.send(); ajax.send();
ajax.addEventListener("error", () => { ajax.addEventListener("error", () => {
@@ -153,50 +152,47 @@ export class Controller {
controller.updateFileList() controller.updateFileList()
} }
updateRTCData(): Promise<void> { async updateRTCData(): Promise<void> {
return fetch(PUBLIC_URL + "/time") try {
.then(response => response.json()) const response = await fetch(PUBLIC_URL + "/time");
.then(json => json as GetTime) const json = await response.json();
.then(time => { const time = json as GetTime;
controller.timeView.update(time.native, time.rtc) controller.timeView.update(time.native, time.rtc);
}) } catch (error) {
.catch(error => { controller.timeView.update("n/a", "n/a");
controller.timeView.update("n/a", "n/a")
console.log(error); console.log(error);
}); }
} }
updateBatteryData(): Promise<void> { async updateBatteryData(): Promise<void> {
return fetch(PUBLIC_URL + "/battery") try {
.then(response => response.json()) const response = await fetch(PUBLIC_URL + "/battery");
.then(json => json as BatteryState) const json = await response.json();
.then(battery => { const battery = json as BatteryState;
controller.batteryView.update(battery) controller.batteryView.update(battery);
}) } catch (error) {
.catch(error => { controller.batteryView.update(null);
controller.batteryView.update(null)
console.log(error); console.log(error);
}) }
} }
updateSolarData(): Promise<void> { async updateSolarData(): Promise<void> {
return fetch(PUBLIC_URL + "/solar") try {
.then(response => response.json()) const response = await fetch(PUBLIC_URL + "/solar");
.then(json => json as SolarState) const json = await response.json();
.then(solar => { const solar = json as SolarState;
controller.solarView.update(solar) controller.solarView.update(solar);
}) } catch (error) {
.catch(error => { controller.solarView.update(null);
controller.solarView.update(null)
console.log(error); console.log(error);
}) }
} }
uploadNewFirmware(file: File) { uploadNewFirmware(file: File) {
var current = 0; let current = 0;
var max = 100; let max = 100;
controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")") controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")")
var ajax = new XMLHttpRequest(); const ajax = new XMLHttpRequest();
ajax.upload.addEventListener("progress", event => { ajax.upload.addEventListener("progress", event => {
current = event.loaded / 1000; current = event.loaded / 1000;
max = event.total / 1000; max = event.total / 1000;
@@ -218,15 +214,13 @@ export class Controller {
ajax.send(file); ajax.send(file);
} }
version(): Promise<void> { async version(): Promise<void> {
controller.progressview.addIndeterminate("version", "Getting buildVersion") controller.progressview.addIndeterminate("version", "Getting buildVersion")
return fetch(PUBLIC_URL + "/version") const response = await fetch(PUBLIC_URL + "/version");
.then(response => response.json()) const json = await response.json();
.then(json => json as VersionInfo) const versionInfo = json as VersionInfo;
.then(versionInfo => { controller.progressview.removeProgress("version");
controller.progressview.removeProgress("version")
controller.firmWareView.setVersion(versionInfo); controller.firmWareView.setVersion(versionInfo);
})
} }
getBackupConfig() { getBackupConfig() {
@@ -243,7 +237,7 @@ export class Controller {
controller.progressview.addIndeterminate("get_config", "Downloading Config") controller.progressview.addIndeterminate("get_config", "Downloading Config")
const response = await fetch(PUBLIC_URL + "/get_config"); const response = await fetch(PUBLIC_URL + "/get_config");
const loaded = await response.json(); const loaded = await response.json();
var currentConfig = loaded as PlantControllerConfig; const currentConfig = loaded as PlantControllerConfig;
controller.setInitialConfig(currentConfig); controller.setInitialConfig(currentConfig);
controller.setConfig(currentConfig); controller.setConfig(currentConfig);
//sync json view initially //sync json view initially
@@ -263,17 +257,18 @@ export class Controller {
}) })
.then(response => response.text()) .then(response => response.text())
.then(text => statusCallback(text)) .then(text => statusCallback(text))
controller.progressview.removeProgress("set_config") .then( _ => {
//load from remote to be clean controller.progressview.removeProgress("set_config");
controller.downloadConfig() setTimeout(() => { controller.downloadConfig() }, 250)
})
} }
backupConfig(json: string): Promise<string> { async backupConfig(json: string): Promise<string> {
return fetch(PUBLIC_URL + "/backup_config", { const response = await fetch(PUBLIC_URL + "/backup_config", {
method: "POST", method: "POST",
body: json, body: json,
}) });
.then(response => response.text()); return await response.text();
} }
syncRTCFromBrowser() { syncRTCFromBrowser() {
@@ -310,9 +305,9 @@ export class Controller {
} }
testNightLamp(active: boolean) { testNightLamp(active: boolean) {
var body: NightLampCommand = { const body: NightLampCommand = {
active: active active: active
} };
var pretty = JSON.stringify(body, undefined, 1); var pretty = JSON.stringify(body, undefined, 1);
fetch(PUBLIC_URL + "/lamptest", { fetch(PUBLIC_URL + "/lamptest", {
method: "POST", method: "POST",

View File

@@ -3,6 +3,7 @@ const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
const CopyPlugin = require("copy-webpack-plugin"); const CopyPlugin = require("copy-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const isDevServer = process.env.WEBPACK_SERVE; const isDevServer = process.env.WEBPACK_SERVE;
console.log("Dev server is " + isDevServer); console.log("Dev server is " + isDevServer);
@@ -30,6 +31,12 @@ module.exports = {
title: "PlantCtrl", title: "PlantCtrl",
}), }),
new HtmlWebpackHarddiskPlugin(), new HtmlWebpackHarddiskPlugin(),
new CompressionPlugin({
algorithm: "gzip",
test: /\.js$|\.css$|\.html$/,
threshold: 0,
minRatio: 0.8
})
], ],
module: { module: {
rules: [ rules: [
@@ -51,6 +58,5 @@ module.exports = {
filename: 'bundle.js', filename: 'bundle.js',
path: path.resolve(__dirname, '.'), path: path.resolve(__dirname, '.'),
}, },
devServer: { devServer: {}
}
}; };