2025-06-19 13:05:47 +02:00

533 lines
20 KiB
Rust

use crate::config::{NetworkConfig, PlantControllerConfig};
use crate::hal::{CONSECUTIVE_WATERING_PLANT, LAST_WATERING_TIMESTAMP, LOW_VOLTAGE_DETECTED, PLANT_COUNT, RESTART_TO_CONF};
use crate::log::{log, LogMessage};
use anyhow::{anyhow, bail, Context};
use embedded_svc::ipv4::IpInfo;
use embedded_svc::wifi::{AccessPointConfiguration, AuthMethod, ClientConfiguration, Configuration};
use esp_idf_hal::delay::Delay;
use esp_idf_hal::gpio::PinDriver;
use esp_idf_svc::mqtt::client::{EspMqttClient, LwtConfiguration, MqttClientConfiguration};
use esp_idf_svc::wifi::EspWifi;
use esp_idf_sys::{esp_spiffs_info, vTaskDelay};
use std::ffi::CString;
use std::fs;
use std::fs::File;
use std::path::Path;
use std::result::Result::Ok as OkStd;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use embedded_svc::mqtt::client::QoS::{AtLeastOnce, ExactlyOnce};
use esp_idf_hal::i2c::I2cDriver;
use serde::Serialize;
use crate::STAY_ALIVE;
#[derive(Serialize, Debug)]
pub struct FileInfo {
filename: String,
size: usize,
}
#[derive(Serialize, Debug)]
pub struct FileList {
total: usize,
used: usize,
files: Vec<FileInfo>,
file_system_corrupt: Option<String>,
iter_error: Option<String>,
}
pub struct FileSystemSizeInfo {
pub total_size: usize,
pub used_size: usize,
pub free_size: usize,
}
pub struct MqttClient<'a> {
mqtt_client: EspMqttClient<'a>,
base_topic: heapless::String<64>
}
pub struct ESP<'a> {
pub(crate) mqtt_client: Option<MqttClient<'a>>,
pub(crate) wifi_driver: EspWifi<'a>,
pub(crate) boot_button: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>,
pub(crate) delay: Delay
}
impl ESP<'_> {
const SPIFFS_PARTITION_NAME: &'static str = "storage";
const CONFIG_FILE: &'static str = "/spiffs/config.cfg";
const BASE_PATH: &'static str = "/spiffs";
pub(crate) fn wifi_ap(&mut self) -> anyhow::Result<()> {
let ssid = match self.get_config(){
Ok(config) => {
config.network.ap_ssid.clone()
}
Err(_) => {
heapless::String::from_str("PlantCtrl Emergency Mode").unwrap()
}
};
let apconfig = AccessPointConfiguration {
ssid,
auth_method: AuthMethod::None,
ssid_hidden: false,
..Default::default()
};
self.wifi_driver.set_configuration(&Configuration::AccessPoint(apconfig))?;
self.wifi_driver.start()?;
anyhow::Ok(())
}
pub(crate) fn wifi(
&mut self,
network_config: &NetworkConfig
) -> anyhow::Result<IpInfo> {
let ssid = network_config.ssid.clone().ok_or(anyhow!("No ssid configured"))?;
let password = network_config.password.clone();
let max_wait = network_config.max_wait;
match password {
Some(pw) => {
//TODO expect error due to invalid pw or similar! //call this during configuration and check if works, revert to config mode if not
self.wifi_driver.set_configuration(&Configuration::Client(
ClientConfiguration {
ssid,
password: pw,
..Default::default()
},
))?;
}
None => {
self.wifi_driver.set_configuration(&Configuration::Client(
ClientConfiguration {
ssid,
auth_method: AuthMethod::None,
..Default::default()
},
))?;
}
}
self.wifi_driver.start()?;
self.wifi_driver.connect()?;
let delay = Delay::new_default();
let mut counter = 0_u32;
while !self.wifi_driver.is_connected()? {
delay.delay_ms(250);
counter += 250;
if counter > max_wait {
//ignore these errors, Wi-Fi will not be used this
self.wifi_driver.disconnect().unwrap_or(());
self.wifi_driver.stop().unwrap_or(());
bail!("Did not manage wifi connection within timeout");
}
}
println!("Should be connected now, waiting for link to be up");
while !self.wifi_driver.is_up()? {
delay.delay_ms(250);
counter += 250;
if counter > max_wait {
//ignore these errors, Wi-Fi will not be used this
self.wifi_driver.disconnect().unwrap_or(());
self.wifi_driver.stop().unwrap_or(());
bail!("Did not manage wifi connection within timeout");
}
}
//update freertos registers ;)
let address = self.wifi_driver.sta_netif().get_ip_info()?;
log(LogMessage::WifiInfo, 0, 0, "", &format!("{address:?}"));
anyhow::Ok(address)
}
pub(crate) fn get_config(&mut self) -> anyhow::Result<PlantControllerConfig> {
let cfg = File::open(Self::CONFIG_FILE)?;
let config: PlantControllerConfig = serde_json::from_reader(cfg)?;
anyhow::Ok(config)
}
pub(crate) fn set_config(&mut self, config: &PlantControllerConfig) -> anyhow::Result<()> {
let mut cfg = File::create(Self::CONFIG_FILE)?;
serde_json::to_writer(&mut cfg, &config)?;
println!("Wrote config config {:?}", config);
anyhow::Ok(())
}
pub(crate) fn delete_config(&self) -> anyhow::Result<()> {
let config = Path::new(Self::CONFIG_FILE);
if config.exists() {
println!("Removing config");
fs::remove_file(config)?
}
anyhow::Ok(())
}
pub(crate) fn mount_file_system(&mut self) -> anyhow::Result<()> {
log(LogMessage::MountingFilesystem, 0, 0, "", "");
let base_path = CString::new("/spiffs")?;
let storage = CString::new(Self::SPIFFS_PARTITION_NAME)?;
let conf = esp_idf_sys::esp_vfs_spiffs_conf_t {
base_path: base_path.as_ptr(),
partition_label: storage.as_ptr(),
max_files: 5,
format_if_mount_failed: true,
};
unsafe {
esp_idf_sys::esp!(esp_idf_sys::esp_vfs_spiffs_register(&conf))?;
}
let free_space = self.file_system_size()?;
log(
LogMessage::FilesystemMount,
free_space.free_size as u32,
free_space.total_size as u32,
&free_space.used_size.to_string(),
"",
);
anyhow::Ok(())
}
fn file_system_size(&mut self) -> anyhow::Result<FileSystemSizeInfo> {
let storage = CString::new(Self::SPIFFS_PARTITION_NAME)?;
let mut total_size = 0;
let mut used_size = 0;
unsafe {
esp_idf_sys::esp!(esp_spiffs_info(
storage.as_ptr(),
&mut total_size,
&mut used_size
))?;
}
anyhow::Ok(FileSystemSizeInfo {
total_size,
used_size,
free_size: total_size - used_size,
})
}
pub(crate) fn list_files(&self) -> FileList {
let storage = CString::new(Self::SPIFFS_PARTITION_NAME).unwrap();
let mut file_system_corrupt = None;
let mut iter_error = None;
let mut result = Vec::new();
let filepath = Path::new(Self::BASE_PATH);
let read_dir = fs::read_dir(filepath);
match read_dir {
OkStd(read_dir) => {
for item in read_dir {
match item {
OkStd(file) => {
let f = FileInfo {
filename: file.file_name().into_string().unwrap(),
size: file
.metadata()
.and_then(|it| anyhow::Result::Ok(it.len()))
.unwrap_or_default()
as usize,
};
result.push(f);
}
Err(err) => {
iter_error = Some(format!("{err:?}"));
break;
}
}
}
}
Err(err) => {
file_system_corrupt = Some(format!("{err:?}"));
}
}
let mut total: usize = 0;
let mut used: usize = 0;
unsafe {
esp_spiffs_info(storage.as_ptr(), &mut total, &mut used);
}
FileList {
total,
used,
file_system_corrupt,
files: result,
iter_error,
}
}
pub(crate) fn delete_file(&self, filename: &str) -> anyhow::Result<()> {
let filepath = Path::new(Self::BASE_PATH).join(Path::new(filename));
match fs::remove_file(filepath) {
OkStd(_) => anyhow::Ok(()),
Err(err) => {
bail!(format!("{err:?}"))
}
}
}
pub(crate) fn get_file_handle(&self, filename: &str, write: bool) -> anyhow::Result<File> {
let filepath = Path::new(Self::BASE_PATH).join(Path::new(filename));
anyhow::Ok(if write {
File::create(filepath)?
} else {
File::open(filepath)?
})
}
pub(crate) fn init_rtc_deepsleep_memory(&self, init_rtc_store: bool, to_config_mode: bool){
if init_rtc_store {
unsafe {
LAST_WATERING_TIMESTAMP = [0; PLANT_COUNT];
CONSECUTIVE_WATERING_PLANT = [0; PLANT_COUNT];
LOW_VOLTAGE_DETECTED = false;
crate::log::init();
RESTART_TO_CONF = to_config_mode;
};
} else {
unsafe {
if to_config_mode {
RESTART_TO_CONF = true;
}
log(
LogMessage::RestartToConfig,
RESTART_TO_CONF as u32,
0,
"",
"",
);
log(
LogMessage::LowVoltage,
LOW_VOLTAGE_DETECTED as u32,
0,
"",
"",
);
for i in 0..PLANT_COUNT {
println!(
"LAST_WATERING_TIMESTAMP[{}] = UTC {}",
i, LAST_WATERING_TIMESTAMP[i]
);
}
for i in 0..PLANT_COUNT {
println!(
"CONSECUTIVE_WATERING_PLANT[{}] = {}",
i, CONSECUTIVE_WATERING_PLANT[i]
);
}
}
}
}
pub(crate) fn mqtt(&mut self, network_config: &NetworkConfig) -> anyhow::Result<()> {
let base_topic = network_config
.base_topic
.as_ref()
.context("missing base topic")?;
if base_topic.is_empty() {
bail!("Mqtt base_topic was empty")
}
let base_topic_copy = base_topic.clone();
let mqtt_url = network_config
.mqtt_url
.as_ref()
.context("missing mqtt url")?;
if mqtt_url.is_empty() {
bail!("Mqtt url was empty")
}
let last_will_topic = format!("{}/state", base_topic);
let mqtt_client_config = MqttClientConfiguration {
lwt: Some(LwtConfiguration {
topic: &last_will_topic,
payload: "lost".as_bytes(),
qos: AtLeastOnce,
retain: true,
}),
client_id: Some("plantctrl"),
keep_alive_interval: Some(Duration::from_secs(60 * 60 * 2)),
//room for improvement
..Default::default()
};
let mqtt_connected_event_received = Arc::new(AtomicBool::new(false));
let mqtt_connected_event_ok = Arc::new(AtomicBool::new(false));
let round_trip_ok = Arc::new(AtomicBool::new(false));
let round_trip_topic = format!("{}/internal/roundtrip", base_topic);
let stay_alive_topic = format!("{}/stay_alive", base_topic);
log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic);
let mqtt_connected_event_received_copy = mqtt_connected_event_received.clone();
let mqtt_connected_event_ok_copy = mqtt_connected_event_ok.clone();
let stay_alive_topic_copy = stay_alive_topic.clone();
let round_trip_topic_copy = round_trip_topic.clone();
let round_trip_ok_copy = round_trip_ok.clone();
let client_id = mqtt_client_config.client_id.unwrap_or("not set");
log(LogMessage::MqttInfo, 0, 0, client_id, mqtt_url);
let mut client = EspMqttClient::new_cb(mqtt_url, &mqtt_client_config, move |event| {
let payload = event.payload();
match payload {
embedded_svc::mqtt::client::EventPayload::Received {
id: _,
topic,
data,
details: _,
} => {
let data = String::from_utf8_lossy(data);
if let Some(topic) = topic {
//todo use enums
if topic.eq(round_trip_topic_copy.as_str()) {
round_trip_ok_copy.store(true, std::sync::atomic::Ordering::Relaxed);
} else if topic.eq(stay_alive_topic_copy.as_str()) {
let value =
data.eq_ignore_ascii_case("true") || data.eq_ignore_ascii_case("1");
log(LogMessage::MqttStayAliveRec, 0, 0, &data, "");
STAY_ALIVE.store(value, std::sync::atomic::Ordering::Relaxed);
} else {
log(LogMessage::UnknownTopic, 0, 0, "", topic);
}
}
}
esp_idf_svc::mqtt::client::EventPayload::Connected(_) => {
mqtt_connected_event_received_copy
.store(true, std::sync::atomic::Ordering::Relaxed);
mqtt_connected_event_ok_copy.store(true, std::sync::atomic::Ordering::Relaxed);
println!("Mqtt connected");
}
esp_idf_svc::mqtt::client::EventPayload::Disconnected => {
mqtt_connected_event_received_copy
.store(true, std::sync::atomic::Ordering::Relaxed);
mqtt_connected_event_ok_copy.store(false, std::sync::atomic::Ordering::Relaxed);
println!("Mqtt disconnected");
}
esp_idf_svc::mqtt::client::EventPayload::Error(esp_error) => {
println!("EspMqttError reported {:?}", esp_error);
mqtt_connected_event_received_copy
.store(true, std::sync::atomic::Ordering::Relaxed);
mqtt_connected_event_ok_copy.store(false, std::sync::atomic::Ordering::Relaxed);
println!("Mqtt error");
}
esp_idf_svc::mqtt::client::EventPayload::BeforeConnect => {
println!("Mqtt before connect")
}
esp_idf_svc::mqtt::client::EventPayload::Subscribed(_) => {
println!("Mqtt subscribed")
}
esp_idf_svc::mqtt::client::EventPayload::Unsubscribed(_) => {
println!("Mqtt unsubscribed")
}
esp_idf_svc::mqtt::client::EventPayload::Published(_) => {
println!("Mqtt published")
}
esp_idf_svc::mqtt::client::EventPayload::Deleted(_) => {
println!("Mqtt deleted")
}
}
})?;
let mut wait_for_connections_event = 0;
while wait_for_connections_event < 100 {
wait_for_connections_event += 1;
match mqtt_connected_event_received.load(std::sync::atomic::Ordering::Relaxed) {
true => {
println!("Mqtt connection callback received, progressing");
match mqtt_connected_event_ok.load(std::sync::atomic::Ordering::Relaxed) {
true => {
println!("Mqtt did callback as connected, testing with roundtrip now");
//subscribe to roundtrip
client.subscribe(round_trip_topic.as_str(), ExactlyOnce)?;
client.subscribe(stay_alive_topic.as_str(), ExactlyOnce)?;
//publish to roundtrip
client.publish(
round_trip_topic.as_str(),
ExactlyOnce,
false,
"online_test".as_bytes(),
)?;
let mut wait_for_roundtrip = 0;
while wait_for_roundtrip < 100 {
wait_for_roundtrip += 1;
match round_trip_ok.load(std::sync::atomic::Ordering::Relaxed) {
true => {
println!("Round trip registered, proceeding");
self.mqtt_client = Some(MqttClient{
mqtt_client: client,
base_topic: base_topic_copy
});
return anyhow::Ok(());
}
false => {
unsafe { vTaskDelay(10) };
}
}
}
bail!("Mqtt did not complete roundtrip in time");
}
false => {
bail!("Mqtt did respond but with failure")
}
}
}
false => {
unsafe { vTaskDelay(10) };
}
}
}
bail!("Mqtt did not fire connection callback in time");
}
pub(crate) fn mqtt_publish(
&mut self,
subtopic: &str,
message: &[u8],
) -> anyhow::Result<()> {
if self.mqtt_client.is_none() {
return anyhow::Ok(());
}
if !subtopic.starts_with("/") {
println!("Subtopic without / at start {}", subtopic);
bail!("Subtopic without / at start {}", subtopic);
}
if subtopic.len() > 192 {
println!("Subtopic exceeds 192 chars {}", subtopic);
bail!("Subtopic exceeds 192 chars {}", subtopic);
}
let client = self.mqtt_client.as_mut().unwrap();
let mut full_topic: heapless::String<256> = heapless::String::new();
if full_topic
.push_str(client.base_topic.as_str())
.is_err()
{
println!("Some error assembling full_topic 1");
bail!("Some error assembling full_topic 1")
};
if full_topic.push_str(subtopic).is_err() {
println!("Some error assembling full_topic 2");
bail!("Some error assembling full_topic 2")
};
let publish = client.mqtt_client.publish(&full_topic, ExactlyOnce, true, message);
Delay::new(10).delay_ms(50);
match publish {
OkStd(message_id) => {
println!(
"Published mqtt topic {} with message {:#?} msgid is {:?}",
full_topic,
String::from_utf8_lossy(message),
message_id
);
anyhow::Ok(())
}
Err(err) => {
println!(
"Error during mqtt send on topic {} with message {:#?} error is {:?}",
full_topic,
String::from_utf8_lossy(message),
err
);
Err(err)?
}
}
}
}