use libmodbus_rs::{Modbus, ModbusClient, ModbusRTU}; use prometheus::core::{AtomicI64, GenericGauge}; use crate::{charge_controllers::gauges::*, errors::TristarError}; const DEVICE_ID: u8 = 0x01; const RAM_DATA_SIZE: u16 = 0x005B; const RAM_ARRAY_SIZE: usize = RAM_DATA_SIZE as usize + 1; #[derive(Debug, Clone)] pub struct Scaling { pub v_scale: f64, pub i_scale: f64, } impl Scaling { fn from(data: &[u16]) -> Self { Self::from_internal(data[0], data[1], data[2], data[3]) } fn from_internal(v_pu_hi: u16, v_pu_lo: u16, i_pu_hi: u16, i_pu_lo: u16) -> Self { Self { v_scale: v_pu_hi as f64 + (v_pu_lo as f64 / f64::powf(2., 16.)), i_scale: i_pu_hi as f64 + (i_pu_lo as f64 / f64::powf(2., 16.)), } } fn get_voltage(&self, data: u16) -> f64 { data as f64 * self.v_scale * f64::powf(2., -15.) } fn get_current(&self, data: u16) -> f64 { data as f64 * self.i_scale * f64::powf(2., -15.) } fn get_power(&self, data: u16) -> f64 { data as f64 * self.v_scale * self.i_scale * f64::powf(2., -17.) } } pub struct Tristar { state: TristarState, port_name: String, modbus: Modbus, data_in: [u16; RAM_ARRAY_SIZE], charge_state_gauges: ChargeStateGauges, } #[derive(Default, Debug, Clone, Copy)] pub struct TristarState { battery_voltage: f64, target_voltage: f64, input_current: f64, battery_temp: u16, charge_state: ChargeState, tristar_input_voltage: f64, tristar_charge_current: f64, tristar_power_out: f64, tristar_power_in: f64, tristar_max_array_power: f64, tristar_max_array_voltage: f64, tristar_open_circuit_voltage: f64, } impl TristarState { fn from_ram(scaling: Scaling, ram: &[u16]) -> Self { Self { battery_voltage: scaling.get_voltage(ram[TristarRamAddress::AdcVbFMed]), target_voltage: scaling.get_voltage(ram[TristarRamAddress::VbRef]), input_current: scaling.get_current(ram[TristarRamAddress::AdcIaFShadow]), battery_temp: ram[TristarRamAddress::Tbatt], charge_state: ChargeState::from(ram[TristarRamAddress::ChargeState]), tristar_input_voltage: scaling.get_voltage(ram[TristarRamAddress::AdcVaF]), tristar_charge_current: scaling.get_current(ram[TristarRamAddress::AdcIbFShadow]), tristar_power_out: scaling.get_power(ram[TristarRamAddress::PowerOutShadow]), tristar_power_in: scaling.get_power(ram[TristarRamAddress::PowerInShadow]), tristar_max_array_power: scaling.get_power(ram[TristarRamAddress::SweepPinMax]), tristar_max_array_voltage: scaling.get_voltage(ram[TristarRamAddress::SweepVmp]), tristar_open_circuit_voltage: scaling.get_voltage(ram[TristarRamAddress::SweepVoc]), } } } #[derive(Debug, Clone, Copy)] enum ChargeState { Start, NightCheck, Disconnect, Night, Fault, Mppt, Absorption, Float, Equalize, Slave, Unknown, } impl Default for ChargeState { fn default() -> Self { Self::Unknown } } impl From for ChargeState { fn from(value: u16) -> Self { match value { 0 => Self::Start, 1 => Self::NightCheck, 2 => Self::Disconnect, 3 => Self::Night, 4 => Self::Fault, 5 => Self::Mppt, 6 => Self::Absorption, 7 => Self::Float, 8 => Self::Equalize, 9 => Self::Slave, _ => Self::Unknown, } } } struct ChargeStateGauges { start: GenericGauge, night_check: GenericGauge, disconnect: GenericGauge, night: GenericGauge, fault: GenericGauge, mppt: GenericGauge, absorption: GenericGauge, float: GenericGauge, equalize: GenericGauge, slave: GenericGauge, unknown: GenericGauge, } impl ChargeStateGauges { fn new(label: &str) -> Self { let start = CHARGE_STATE.with_label_values(&[label, "start"]); let night_check = CHARGE_STATE.with_label_values(&[label, "night_check"]); let disconnect = CHARGE_STATE.with_label_values(&[label, "disconnect"]); let night = CHARGE_STATE.with_label_values(&[label, "night"]); let fault = CHARGE_STATE.with_label_values(&[label, "fault"]); let mppt = CHARGE_STATE.with_label_values(&[label, "mppt"]); let absorption = CHARGE_STATE.with_label_values(&[label, "absorption"]); let float = CHARGE_STATE.with_label_values(&[label, "float"]); let equalize = CHARGE_STATE.with_label_values(&[label, "equalize"]); let slave = CHARGE_STATE.with_label_values(&[label, "slave"]); let unknown = CHARGE_STATE.with_label_values(&[label, "unknown"]); Self { start, night_check, disconnect, night, fault, mppt, absorption, float, equalize, slave, unknown, } } fn zero_all(&mut self) { self.start.set(0); self.night_check.set(0); self.disconnect.set(0); self.night.set(0); self.fault.set(0); self.mppt.set(0); self.absorption.set(0); self.float.set(0); self.equalize.set(0); self.slave.set(0); self.unknown.set(0); } fn set(&mut self, state: ChargeState) { match state { ChargeState::Start => { self.zero_all(); self.start.set(1); } ChargeState::NightCheck => { self.zero_all(); self.night_check.set(1); } ChargeState::Disconnect => { self.zero_all(); self.disconnect.set(1); } ChargeState::Night => { self.zero_all(); self.night.set(1); } ChargeState::Fault => { self.zero_all(); self.fault.set(1); } ChargeState::Mppt => { self.zero_all(); self.mppt.set(1); } ChargeState::Absorption => { self.zero_all(); self.absorption.set(1); } ChargeState::Float => { self.zero_all(); self.float.set(1); } ChargeState::Equalize => { self.zero_all(); self.equalize.set(1); } ChargeState::Slave => { self.zero_all(); self.slave.set(1); } ChargeState::Unknown => { self.zero_all(); self.unknown.set(1); } } } } impl Tristar { pub fn new(serial_port: String, baud_rate: i32) -> Result { let parity = 'N'; let data_bit = 8; let stop_bit = 2; let mut modbus = Modbus::new_rtu(&serial_port, baud_rate, parity, data_bit, stop_bit)?; modbus.set_slave(DEVICE_ID)?; modbus.connect()?; let charge_state_gauges = ChargeStateGauges::new(&serial_port); Ok(Self { state: Default::default(), port_name: serial_port, modbus, data_in: [0; RAM_ARRAY_SIZE], charge_state_gauges, }) } pub fn refresh(&mut self) { if let Ok(new_state) = self .get_data() .map(|scaling| TristarState::from_ram(scaling, &self.data_in)) { BATTERY_VOLTAGE .with_label_values(&[&self.port_name]) .set(new_state.battery_voltage); TARGET_VOLTAGE .with_label_values(&[&self.port_name]) .set(new_state.target_voltage); INPUT_CURRENT .with_label_values(&[&self.port_name]) .set(new_state.input_current); BATTERY_TEMP .with_label_values(&[&self.port_name]) .set(new_state.battery_temp.into()); TRISTAR_INPUT_VOLTAGE .with_label_values(&[&self.port_name]) .set(new_state.tristar_input_voltage); TRISTAR_CHARGE_CURRENT .with_label_values(&[&self.port_name]) .set(new_state.tristar_charge_current); TRISTAR_POWER_OUT .with_label_values(&[&self.port_name]) .set(new_state.tristar_power_out); TRISTAR_POWER_IN .with_label_values(&[&self.port_name]) .set(new_state.tristar_power_in); TRISTAR_MAX_ARRAY_POWER .with_label_values(&[&self.port_name]) .set(new_state.tristar_max_array_power); TRISTAR_MAX_ARRAY_VOLTAGE .with_label_values(&[&self.port_name]) .set(new_state.tristar_max_array_voltage); TRISTAR_OPEN_CIRCUIT_VOLTAGE .with_label_values(&[&self.port_name]) .set(new_state.tristar_open_circuit_voltage); self.charge_state_gauges.set(new_state.charge_state); self.state = new_state; } } fn get_data(&mut self) -> Result { self.modbus .read_registers(0x0000, RAM_DATA_SIZE + 1, &mut self.data_in)?; let scaling = Scaling::from(&self.data_in); Ok(scaling) } } enum TristarRamAddress { AdcVbFMed = 25, AdcVaF = 28, Tbatt = 38, AdcIbFShadow = 29, AdcIaFShadow = 30, ChargeState = 51, VbRef = 52, PowerOutShadow = 59, PowerInShadow = 60, SweepPinMax = 61, SweepVmp = 62, SweepVoc = 63, } impl std::ops::Index for [u16] { type Output = u16; fn index(&self, index: TristarRamAddress) -> &Self::Output { &self[index as usize] } }