refactor: simplify battery monitoring logic, remove unused fields, and replace BQ34Z100G1 integration with WCH I2C implementation

This commit is contained in:
2026-01-04 23:59:14 +01:00
parent 350820741a
commit 8fc2a89503
12 changed files with 105 additions and 281 deletions

View File

@@ -3,6 +3,7 @@
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/rust/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/rust/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/rust/target" />
</content>
<orderEntry type="inheritedJdk" />

View File

@@ -95,12 +95,12 @@ embedded-can = "0.4.1"
nb = "1.1.0"
# Concrete hardware drivers and sensors/IO expanders
lib-bms-protocol = {git = "https://gitea.wlandt.de/judge/ch32-bms.git" , default-features = false }
onewire = "0.4.0"
ds323x = "0.7.0"
eeprom24x = "0.7.2"
pca9535 = { version = "2.0.0" }
ina219 = { version = "0.2.0" }
bq34z100 = { version = "0.4.0", default-features = false }
# Storage and filesystem
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] }

View File

@@ -83,7 +83,6 @@ impl Default for TankConfig {
pub enum BatteryBoardVersion {
#[default]
Disabled,
BQ34Z100G1,
WchI2cSlave,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]

View File

@@ -2,6 +2,7 @@ use alloc::format;
use alloc::string::{String, ToString};
use core::convert::Infallible;
use core::fmt;
use core::fmt::Debug;
use core::str::Utf8Error;
use embassy_embedded_hal::shared_bus::I2cDeviceError;
use embassy_executor::SpawnError;
@@ -12,6 +13,7 @@ use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit};
use esp_hal::twai::EspTwaiError;
use esp_radio::wifi::WifiError;
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
use lib_bms_protocol::BmsProtocolError;
use littlefs2_core::PathError;
use onewire::Error;
use pca9535::ExpanderError;
@@ -19,6 +21,9 @@ use pca9535::ExpanderError;
//All error superconstruct
#[derive(Debug)]
pub enum FatError {
BMSError {
error: String,
},
OneWireError {
error: Error<Infallible>,
},
@@ -100,6 +105,7 @@ impl fmt::Display for FatError {
write!(f, "CanBusError {error:?}")
}
FatError::SNTPError { error } => write!(f, "SNTPError {error:?}"),
FatError::BMSError { error } => write!(f, "BMSError, {error}"),
}
}
}
@@ -316,3 +322,14 @@ impl From<sntpc::Error> for FatError {
FatError::SNTPError { error: value }
}
}
impl From<BmsProtocolError> for FatError{
fn from(value: BmsProtocolError) -> Self {
match value {
BmsProtocolError::I2cCommunicationError => {
FatError::String{error: "I2C communication error".to_string()}
}
}
}
}

View File

@@ -1,39 +1,30 @@
use crate::fat_error::{FatError, FatResult};
use crate::hal::Box;
use crate::fat_error::{FatError, FatResult};
use async_trait::async_trait;
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 lib_bms_protocol::{BatteryState as bstate, BmsReadable, Config, FirmwareVersion, ProtocolVersion};
use serde::Serialize;
#[async_trait(?Send)]
pub trait BatteryInteraction {
async fn state_charge_percent(&mut self) -> FatResult<f32>;
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16>;
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16>;
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16>;
async fn voltage_milli_volt(&mut self) -> FatResult<u16>;
async fn average_current_milli_ampere(&mut self) -> FatResult<i16>;
async fn cycle_count(&mut self) -> FatResult<u16>;
async fn state_health_percent(&mut self) -> FatResult<u16>;
async fn bat_temperature(&mut self) -> FatResult<u16>;
async fn get_battery_state(&mut self) -> FatResult<BatteryState>;
async fn get_state(&mut self) -> FatResult<BatteryState>;
async fn get_firmware(&mut self) -> FatResult<FirmwareVersion>;
async fn get_protocol(&mut self) -> FatResult<ProtocolVersion>;
async fn reset(&mut self) -> FatResult<()>;
}
#[derive(Debug, Serialize)]
pub struct BatteryInfo {
pub voltage_milli_volt: u16,
pub average_current_milli_ampere: i16,
pub cycle_count: u16,
pub design_milli_ampere_hour: u16,
pub remaining_milli_ampere_hour: u16,
pub state_of_charge: f32,
pub state_of_health: u16,
pub temperature: u16,
pub voltage_milli_volt: u32,
pub average_current_milli_ampere: i32,
pub design_milli_ampere_hour: u32,
pub remaining_milli_ampere_hour: u32,
pub state_of_charge: u8,
pub state_of_health: u32,
pub temperature: i32,
}
#[derive(Debug, Serialize)]
@@ -46,213 +37,62 @@ pub enum BatteryState {
pub struct NoBatteryMonitor {}
#[async_trait(?Send)]
impl BatteryInteraction for NoBatteryMonitor {
async fn state_charge_percent(&mut self) -> FatResult<f32> {
// No monitor configured: assume full battery for lightstate logic
Ok(100.0)
}
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn voltage_milli_volt(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn average_current_milli_ampere(&mut self) -> FatResult<i16> {
Err(FatError::NoBatteryMonitor)
}
async fn cycle_count(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn state_health_percent(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn bat_temperature(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn get_battery_state(&mut self) -> FatResult<BatteryState> {
async fn get_state(&mut self) -> FatResult<BatteryState> {
Ok(BatteryState::Unknown)
}
async fn get_firmware(&mut self) -> FatResult<FirmwareVersion> {
Err(FatError::NoBatteryMonitor)
}
async fn get_protocol(&mut self) -> FatResult<ProtocolVersion> {
Err(FatError::NoBatteryMonitor)
}
async fn reset(&mut self) -> FatResult<()> {
Err(FatError::NoBatteryMonitor)
}
}
//TODO implement this battery monitor kind once controller is complete
#[allow(dead_code)]
pub struct WchI2cSlave {}
pub type I2cDev = I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>;
pub struct BQ34Z100G1 {
pub battery_driver: Bq34z100g1Driver<I2cDev, Delay>,
pub struct WCHI2CSlave<'a> {
pub(crate) i2c: I2cDevice<'a, CriticalSectionRawMutex, I2c<'a, Blocking>>
}
#[async_trait(?Send)]
impl BatteryInteraction for BQ34Z100G1 {
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:?}"),
})
}
impl BatteryInteraction for WCHI2CSlave<'_> {
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16> {
self.battery_driver
.remaining_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{e:?}"),
})
}
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16> {
self.battery_driver
.full_charge_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{e:?}"),
})
}
async fn get_state(&mut self) -> FatResult<BatteryState> {
let state = bstate::read_from_i2c(&mut self.i2c)?;
let config = Config::read_from_i2c(&mut self.i2c)?;
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16> {
self.battery_driver
.design_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{e:?}"),
})
}
let state_of_charge = (state.remaining_capacity_mah * 100 / state.lifetime_capacity_mah) as u8;
let state_of_health = state.lifetime_capacity_mah / config.capacity_mah * 100;
async fn voltage_milli_volt(&mut self) -> FatResult<u16> {
self.battery_driver.voltage().map_err(|e| FatError::String {
error: alloc::format!("{e:?}"),
})
}
async fn average_current_milli_ampere(&mut self) -> FatResult<i16> {
self.battery_driver
.average_current()
.map_err(|e| FatError::String {
error: alloc::format!("{e:?}"),
})
}
async fn cycle_count(&mut self) -> FatResult<u16> {
self.battery_driver
.cycle_count()
.map_err(|e| FatError::String {
error: alloc::format!("{e:?}"),
})
}
async fn state_health_percent(&mut self) -> FatResult<u16> {
self.battery_driver
.state_of_health()
.map_err(|e| FatError::String {
error: alloc::format!("{e:?}"),
})
}
async fn bat_temperature(&mut self) -> FatResult<u16> {
self.battery_driver
.temperature()
.map_err(|e| FatError::String {
error: alloc::format!("{e:?}"),
})
}
async fn get_battery_state(&mut self) -> FatResult<BatteryState> {
Ok(BatteryState::Info(BatteryInfo {
voltage_milli_volt: self.voltage_milli_volt().await?,
average_current_milli_ampere: self.average_current_milli_ampere().await?,
cycle_count: self.cycle_count().await?,
design_milli_ampere_hour: self.design_milli_ampere_hour().await?,
remaining_milli_ampere_hour: self.remaining_milli_ampere_hour().await?,
state_of_charge: self.state_charge_percent().await?,
state_of_health: self.state_health_percent().await?,
temperature: self.bat_temperature().await?,
voltage_milli_volt: state.current_mv,
average_current_milli_ampere: 1337,
design_milli_ampere_hour: config.capacity_mah,
remaining_milli_ampere_hour: state.remaining_capacity_mah,
state_of_charge,
state_of_health,
temperature: state.temperature_celcius,
}))
}
}
pub fn print_battery_bq34z100(
battery_driver: &mut Bq34z100g1Driver<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>, Delay>,
) -> FatResult<()> {
log::info!("Try communicating with battery");
let fwversion = battery_driver.fw_version().unwrap_or_else(|e| {
log::info!("Firmware {e:?}");
0
});
log::info!("fw version is {fwversion}");
let design_capacity = battery_driver.design_capacity().unwrap_or_else(|e| {
log::info!("Design capacity {e:?}");
0
});
log::info!("Design Capacity {design_capacity}");
if design_capacity == 1000 {
log::info!("Still stock configuring battery, readouts are likely to be wrong!");
async fn get_firmware(&mut self) -> FatResult<FirmwareVersion> {
todo!()
}
let flags = battery_driver.get_flags_decoded().unwrap_or(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:?}");
async fn get_protocol(&mut self) -> FatResult<ProtocolVersion> {
todo!()
}
let chem_id = battery_driver.chem_id().unwrap_or_else(|e| {
log::info!("Chemid {e:?}");
0
});
let bat_temp = battery_driver.internal_temperature().unwrap_or_else(|e| {
log::info!("Bat Temp {e:?}");
0
});
let temp_c = Temperature::from_kelvin(bat_temp as f64 / 10_f64).as_celsius();
let voltage = battery_driver.voltage().unwrap_or_else(|e| {
log::info!("Bat volt {e:?}");
0
});
let current = battery_driver.current().unwrap_or_else(|e| {
log::info!("Bat current {e:?}");
0
});
let state = battery_driver.state_of_charge().unwrap_or_else(|e| {
log::info!("Bat Soc {e:?}");
0
});
let charge_voltage = battery_driver.charge_voltage().unwrap_or_else(|e| {
log::info!("Bat Charge Volt {e:?}");
0
});
let charge_current = battery_driver.charge_current().unwrap_or_else(|e| {
log::info!("Bat Charge Current {e:?}");
0
});
log::info!("ChemId: {chem_id} Current voltage {voltage} and current {current} with charge {state}% and temp {temp_c} CVolt: {charge_voltage} CCur {charge_current}");
let _ = battery_driver.unsealed();
let _ = battery_driver.it_enable();
Ok(())
}
async fn reset(&mut self) -> FatResult<()> {
todo!()
}
}

View File

@@ -1,3 +1,4 @@
use lib_bms_protocol::BmsReadable;
pub(crate) mod battery;
// mod can_api; // replaced by external canapi crate
pub mod esp;
@@ -8,6 +9,7 @@ mod shared_flash;
mod v4_hal;
mod water;
use lib_bms_protocol::ProtocolVersion;
use crate::alloc::string::ToString;
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
use esp_hal::peripherals::Peripherals;
@@ -50,7 +52,6 @@ use alloc::format;
use alloc::sync::Arc;
use async_trait::async_trait;
use bincode::{Decode, Encode};
use bq34z100::Bq34z100g1Driver;
use canapi::SensorSlot;
use chrono::{DateTime, FixedOffset, Utc};
use core::cell::RefCell;
@@ -72,7 +73,7 @@ use esp_hal::gpio::{Input, InputConfig, Pull};
use measurements::{Current, Voltage};
use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::battery::{print_battery_bq34z100, BQ34Z100G1};
use crate::hal::battery::{WCHI2CSlave};
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use crate::hal::water::TankSensor;
use crate::log::LOG_ACCESS;
@@ -485,6 +486,7 @@ impl PlantHal {
let i2c_bus = I2C_DRIVER.get().await;
let rtc_device = I2cDevice::new(i2c_bus);
let mut bms_device = I2cDevice::new(i2c_bus);
let eeprom_device = I2cDevice::new(i2c_bus);
let mut rtc: Ds323x<
@@ -519,35 +521,18 @@ impl PlantHal {
let battery_interaction: Box<dyn BatteryInteraction + Send> =
match config.hardware.battery {
BatteryBoardVersion::Disabled => Box::new(NoBatteryMonitor {}),
BatteryBoardVersion::BQ34Z100G1 => {
let battery_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut battery_driver = Bq34z100g1Driver {
i2c: battery_device,
delay: Delay::new(),
flash_block_data: [0; 32],
};
let status = print_battery_bq34z100(&mut battery_driver);
match status {
Ok(_) => {}
Err(err) => {
LOG_ACCESS
.lock()
.await
.log(
LogMessage::BatteryCommunicationError,
0u32,
0,
"",
&format!("{err:?})"),
)
.await;
}
}
Box::new(BQ34Z100G1 { battery_driver })
}
BatteryBoardVersion::WchI2cSlave => {
// TODO use correct implementation once availible
Box::new(NoBatteryMonitor {})
let version = ProtocolVersion::read_from_i2c(&mut bms_device);
let version_val = match version {
Ok(v) => unsafe { core::mem::transmute::<ProtocolVersion, u32>(v) },
Err(_) => 0,
};
if version_val == 1 {
Box::new(WCHI2CSlave { i2c: bms_device })
} else {
//todo should be an error variant instead?
Box::new(NoBatteryMonitor {})
}
}
};

View File

@@ -280,6 +280,8 @@ pub enum LogMessage {
PumpOpenLoopCurrent,
#[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")]
PumpMissingSensorCurrent,
#[strum(serialize = "MPPT Current sensor could not be reached")]
MPPTError,
}
#[derive(Serialize)]

View File

@@ -492,22 +492,18 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
info!("state of charg");
let is_day = board.board_hal.is_day();
let state_of_charge = board
.board_hal
.get_battery_monitor()
.state_charge_percent()
.await
.unwrap_or(0.);
// try to load full battery state if failed the battery state is unknown
let battery_state = board
.board_hal
.get_battery_monitor()
.get_battery_state()
.await
.unwrap_or(BatteryState::Unknown);
let battery_state = board.board_hal.get_battery_monitor().get_state().await.unwrap_or(BatteryState::Unknown);
info!("Battery state is {battery_state:?}");
let state_of_charge = match &battery_state {
BatteryState::Unknown => {
0
}
BatteryState::Info(data) => {
data.state_of_charge
}
};
let mut light_state = LightState {
enabled: board.board_hal.get_config().night_lamp.enabled,
..Default::default()
@@ -530,7 +526,6 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.get_config()
.night_lamp
.low_soc_cutoff
.into()
{
board.board_hal.get_esp().set_low_voltage_in_cycle();
info!("Set low voltage in cycle");
@@ -540,7 +535,6 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.get_config()
.night_lamp
.low_soc_restore
.into()
{
board.board_hal.get_esp().clear_low_voltage_in_cycle();
info!("Clear low voltage in cycle");
@@ -591,7 +585,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let deep_sleep_duration_minutes: u32 =
// if battery soc is unknown assume battery has enough change
if state_of_charge < 10.0 && !matches!(battery_state, BatteryState::Unknown) {
if state_of_charge < 10 && !matches!(battery_state, BatteryState::Unknown) {
let _ = board
.board_hal
.get_esp()
@@ -756,24 +750,19 @@ pub async fn do_secure_pump(
async fn update_charge_indicator(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
) {
//FIXME add config and code to allow power supply mode, in this case this is a nop
//we have mppt controller, ask it for charging current
if let Ok(current) = board.board_hal.get_mptt_current().await {
let _ = board
.board_hal
.set_charge_indicator(current.as_milliamperes() > 20_f64)
.await;
}
//fallback to battery controller and ask it instead
else if let Ok(charging) = board
.board_hal
.get_battery_monitor()
.average_current_milli_ampere()
.await
{
let _ = board.board_hal.set_charge_indicator(charging > 20).await;
} else {
//who knows
board.board_hal.general_fault(true).await;
log(LogMessage::MPPTError, 0, 0, "", "").await;
let _ = board.board_hal.set_charge_indicator(false).await;
}
}
@@ -959,7 +948,7 @@ async fn publish_battery_state(
let state = board
.board_hal
.get_battery_monitor()
.get_battery_state()
.get_state()
.await;
let value = match state {
Ok(state) => {

View File

@@ -131,7 +131,7 @@ pub(crate) async fn get_battery_state<T, const N: usize>(
let battery_state = board
.board_hal
.get_battery_monitor()
.get_battery_state()
.get_state()
.await?;
Ok(Some(serde_json::to_string(&battery_state)?))
}

View File

@@ -167,7 +167,6 @@ export interface BatteryState {
temperature: string
voltage_milli_volt: string,
current_milli_ampere: string,
cycle_count: string,
design_milli_ampere: string,
remaining_milli_ampere: string,
state_of_charge: string,

View File

@@ -23,10 +23,6 @@
<span class="powerflexkey">mA:</span>
<span class="powerflexvalue" id="battery_current_milli_ampere" ></span>
</div>
<div class="flexcontainer">
<span class="powerflexkey">Cycles:</span>
<span class="powerflexvalue" id="battery_cycle_count" ></span>
</div>
<div class="flexcontainer">
<span class="powerflexkey">design mA:</span>
<span class="powerflexvalue" id="battery_design_milli_ampere" ></span>

View File

@@ -4,7 +4,6 @@ import {BatteryState} from "./api";
export class BatteryView{
voltage_milli_volt: HTMLSpanElement;
current_milli_ampere: HTMLSpanElement;
cycle_count: HTMLSpanElement;
design_milli_ampere: HTMLSpanElement;
remaining_milli_ampere: HTMLSpanElement;
state_of_charge: HTMLSpanElement;
@@ -18,7 +17,6 @@ export class BatteryView{
(document.getElementById("batteryview") as HTMLElement).innerHTML = require("./batteryview.html")
this.voltage_milli_volt = document.getElementById("battery_voltage_milli_volt") as HTMLSpanElement;
this.current_milli_ampere = document.getElementById("battery_current_milli_ampere") as HTMLSpanElement;
this.cycle_count = document.getElementById("battery_cycle_count") as HTMLSpanElement;
this.design_milli_ampere = document.getElementById("battery_design_milli_ampere") as HTMLSpanElement;
this.remaining_milli_ampere = document.getElementById("battery_remaining_milli_ampere") as HTMLSpanElement;
this.state_of_charge = document.getElementById("battery_state_of_charge") as HTMLSpanElement;
@@ -41,7 +39,6 @@ export class BatteryView{
if (batterystate == null) {
this.voltage_milli_volt.innerText = "N/A"
this.current_milli_ampere.innerText = "N/A"
this.cycle_count.innerText = "N/A"
this.design_milli_ampere.innerText = "N/A"
this.remaining_milli_ampere.innerText = "N/A"
this.state_of_charge.innerText = "N/A"
@@ -50,12 +47,11 @@ export class BatteryView{
} else {
this.voltage_milli_volt.innerText = String(+batterystate.voltage_milli_volt/1000)
this.current_milli_ampere.innerText = batterystate.current_milli_ampere
this.cycle_count.innerText = batterystate.cycle_count
this.design_milli_ampere.innerText = batterystate.design_milli_ampere
this.remaining_milli_ampere.innerText = batterystate.remaining_milli_ampere
this.state_of_charge.innerText = batterystate.state_of_charge
this.state_of_health.innerText = batterystate.state_of_health
this.temperature.innerText = String(+batterystate.temperature / 100)
this.temperature.innerText = String(+batterystate.temperature)
}