2024-01-18 16:26:58 +11:00
|
|
|
use libmodbus_rs::{Modbus, ModbusClient, ModbusRTU};
|
2024-01-28 10:24:00 +11:00
|
|
|
use prometheus::core::{AtomicI64, GenericGauge};
|
2024-01-18 16:26:58 +11:00
|
|
|
|
2024-01-28 10:24:00 +11:00
|
|
|
use crate::{charge_controllers::gauges::*, errors::TristarError};
|
2024-01-18 16:26:58 +11:00
|
|
|
|
|
|
|
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,
|
2024-01-28 10:24:00 +11:00
|
|
|
port_name: String,
|
2024-01-18 16:26:58 +11:00
|
|
|
modbus: Modbus,
|
|
|
|
data_in: [u16; RAM_ARRAY_SIZE],
|
2024-01-28 10:24:00 +11:00
|
|
|
charge_state_gauges: ChargeStateGauges,
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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<u16> 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 {
|
2024-01-28 10:24:00 +11:00
|
|
|
start: GenericGauge<AtomicI64>,
|
|
|
|
night_check: GenericGauge<AtomicI64>,
|
|
|
|
disconnect: GenericGauge<AtomicI64>,
|
|
|
|
night: GenericGauge<AtomicI64>,
|
|
|
|
fault: GenericGauge<AtomicI64>,
|
|
|
|
mppt: GenericGauge<AtomicI64>,
|
|
|
|
absorption: GenericGauge<AtomicI64>,
|
|
|
|
float: GenericGauge<AtomicI64>,
|
|
|
|
equalize: GenericGauge<AtomicI64>,
|
|
|
|
slave: GenericGauge<AtomicI64>,
|
|
|
|
unknown: GenericGauge<AtomicI64>,
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ChargeStateGauges {
|
2024-01-28 10:24:00 +11:00
|
|
|
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"]);
|
2024-01-18 16:26:58 +11:00
|
|
|
Self {
|
|
|
|
start,
|
|
|
|
night_check,
|
|
|
|
disconnect,
|
|
|
|
night,
|
|
|
|
fault,
|
|
|
|
mppt,
|
|
|
|
absorption,
|
|
|
|
float,
|
|
|
|
equalize,
|
|
|
|
slave,
|
|
|
|
unknown,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn zero_all(&mut self) {
|
2024-01-28 10:24:00 +11:00
|
|
|
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);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
fn set(&mut self, state: ChargeState) {
|
|
|
|
match state {
|
|
|
|
ChargeState::Start => {
|
|
|
|
self.zero_all();
|
2024-01-28 10:24:00 +11:00
|
|
|
self.start.set(1);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
ChargeState::NightCheck => {
|
|
|
|
self.zero_all();
|
2024-01-28 10:24:00 +11:00
|
|
|
self.night_check.set(1);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
ChargeState::Disconnect => {
|
|
|
|
self.zero_all();
|
2024-01-28 10:24:00 +11:00
|
|
|
self.disconnect.set(1);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
ChargeState::Night => {
|
|
|
|
self.zero_all();
|
2024-01-28 10:24:00 +11:00
|
|
|
self.night.set(1);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
ChargeState::Fault => {
|
|
|
|
self.zero_all();
|
2024-01-28 10:24:00 +11:00
|
|
|
self.fault.set(1);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
ChargeState::Mppt => {
|
|
|
|
self.zero_all();
|
2024-01-28 10:24:00 +11:00
|
|
|
self.mppt.set(1);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
ChargeState::Absorption => {
|
|
|
|
self.zero_all();
|
2024-01-28 10:24:00 +11:00
|
|
|
self.absorption.set(1);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
ChargeState::Float => {
|
|
|
|
self.zero_all();
|
2024-01-28 10:24:00 +11:00
|
|
|
self.float.set(1);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
ChargeState::Equalize => {
|
|
|
|
self.zero_all();
|
2024-01-28 10:24:00 +11:00
|
|
|
self.equalize.set(1);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
ChargeState::Slave => {
|
|
|
|
self.zero_all();
|
2024-01-28 10:24:00 +11:00
|
|
|
self.slave.set(1);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
ChargeState::Unknown => {
|
|
|
|
self.zero_all();
|
2024-01-28 10:24:00 +11:00
|
|
|
self.unknown.set(1);
|
2024-01-18 16:26:58 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Tristar {
|
|
|
|
pub fn new(serial_port: String, baud_rate: i32) -> Result<Self, TristarError> {
|
|
|
|
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()?;
|
2024-01-28 10:24:00 +11:00
|
|
|
let charge_state_gauges = ChargeStateGauges::new(&serial_port);
|
2024-01-18 16:26:58 +11:00
|
|
|
Ok(Self {
|
|
|
|
state: Default::default(),
|
2024-01-28 10:24:00 +11:00
|
|
|
port_name: serial_port,
|
2024-01-18 16:26:58 +11:00
|
|
|
modbus,
|
|
|
|
data_in: [0; RAM_ARRAY_SIZE],
|
2024-01-28 10:24:00 +11:00
|
|
|
charge_state_gauges,
|
2024-01-18 16:26:58 +11:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn refresh(&mut self) {
|
|
|
|
if let Ok(new_state) = self
|
|
|
|
.get_data()
|
|
|
|
.map(|scaling| TristarState::from_ram(scaling, &self.data_in))
|
|
|
|
{
|
2024-01-28 10:24:00 +11:00
|
|
|
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])
|
2024-01-18 16:26:58 +11:00
|
|
|
.set(new_state.tristar_input_voltage);
|
2024-01-28 10:24:00 +11:00
|
|
|
TRISTAR_CHARGE_CURRENT
|
|
|
|
.with_label_values(&[&self.port_name])
|
2024-01-18 16:26:58 +11:00
|
|
|
.set(new_state.tristar_charge_current);
|
2024-01-28 10:24:00 +11:00
|
|
|
TRISTAR_POWER_OUT
|
|
|
|
.with_label_values(&[&self.port_name])
|
2024-01-18 16:26:58 +11:00
|
|
|
.set(new_state.tristar_power_out);
|
2024-01-28 10:24:00 +11:00
|
|
|
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])
|
2024-01-18 16:26:58 +11:00
|
|
|
.set(new_state.tristar_max_array_power);
|
2024-01-28 10:24:00 +11:00
|
|
|
TRISTAR_MAX_ARRAY_VOLTAGE
|
|
|
|
.with_label_values(&[&self.port_name])
|
2024-01-18 16:26:58 +11:00
|
|
|
.set(new_state.tristar_max_array_voltage);
|
2024-01-28 10:24:00 +11:00
|
|
|
TRISTAR_OPEN_CIRCUIT_VOLTAGE
|
|
|
|
.with_label_values(&[&self.port_name])
|
2024-01-18 16:26:58 +11:00
|
|
|
.set(new_state.tristar_open_circuit_voltage);
|
2024-01-28 10:24:00 +11:00
|
|
|
|
|
|
|
self.charge_state_gauges.set(new_state.charge_state);
|
2024-01-18 16:26:58 +11:00
|
|
|
self.state = new_state;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_data(&mut self) -> Result<Scaling, TristarError> {
|
|
|
|
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<TristarRamAddress> for [u16] {
|
|
|
|
type Output = u16;
|
|
|
|
|
|
|
|
fn index(&self, index: TristarRamAddress) -> &Self::Output {
|
|
|
|
&self[index as usize]
|
|
|
|
}
|
|
|
|
}
|