use std::{io::Write, time::Duration}; use anyhow::Context; use metrics::{describe_gauge, gauge, Gauge}; use serialport::SerialPort; use termcolor::WriteColor; pub struct Pli { port: Box, voltage_gauge: Gauge, duty_cycle: Gauge, internal_charge_current: Gauge, internal_load_current: Gauge, } #[derive(Debug, Clone, Copy)] pub enum PliRequest { ReadRam(u8), } impl Pli { pub fn new(serial_port: String, baud_rate: u32) -> anyhow::Result { let port = serialport::new(serial_port, baud_rate) .timeout(Duration::from_millis(250)) .open()?; describe_gauge!("pl_battery_voltage", "Battery voltage"); let voltage_gauge = gauge!("pl_battery_voltage"); describe_gauge!("pl_duty_cycle", "Duty cycle"); let duty_cycle = gauge!("pl_duty_cycle"); describe_gauge!("pl_internal_charge_current", "Internal charge current"); let internal_charge_current = gauge!("pl_internal_charge_current"); describe_gauge!("pl_internal_load_current", "Internal load current"); let internal_load_current = gauge!("pl_internal_load_current"); Ok(Self { port, voltage_gauge, duty_cycle, internal_charge_current, internal_load_current, }) } pub fn refresh(&mut self) { if let Ok(batv) = self.read_ram(PlRamAddress::Batv) { self.voltage_gauge.set((batv as f64) * (4. / 10.)); } if let Ok(duty_cycle) = self.read_ram(PlRamAddress::Dutycyc) { self.duty_cycle.set((duty_cycle as f64) / 255.); } if let Ok(internal_charge_current) = self.read_ram(PlRamAddress::Cint) { self.internal_charge_current .set((internal_charge_current as f64) * (4. / 10.)); } if let Ok(internal_load_current) = self.read_ram(PlRamAddress::Lint) { self.internal_load_current .set((internal_load_current as f64) * (4. / 10.)); } } pub fn process_request(&mut self, message: PliRequest) { match message { PliRequest::ReadRam(address) => match self.read_ram(address) { Ok(data) => { let mut stdout = termcolor::StandardStream::stdout(termcolor::ColorChoice::Always); let _ = stdout.set_color( termcolor::ColorSpec::new().set_fg(Some(termcolor::Color::Green)), ); if writeln!(&mut stdout, "Read RAM at {address}: data {data}").is_err() { log::warn!( "Failed to set stdout colour\nRead RAM at {address}: data {data}" ); }; } Err(e) => log::error!("RAM read error: {e:?}"), }, } } fn send_command(&mut self, req: [u8; 4]) { self.port .write_all(&req) .expect("failed to write to serial port"); } fn receive(&mut self) -> [u8; LENGTH] { let mut buf = [0; LENGTH]; match self.port.read_exact(&mut buf) { Ok(_) => {} Err(e) => log::error!("PLI serial read error: {e:#?}"), } buf } fn read_ram(&mut self, address: T) -> anyhow::Result where T: Into, { self.send_command(command(20, address.into(), 0)); let buf: [u8; 2] = self.receive(); if buf[0] == 200 { Some(buf[1]) } else { None } .context(format!("Error from PLI: {}", buf[0])) } } enum PlRamAddress { Dutycyc, Batv, Cint, Lint, } impl From for u8 { fn from(value: PlRamAddress) -> Self { match value { PlRamAddress::Dutycyc => 39, PlRamAddress::Batv => 50, PlRamAddress::Cint => 213, PlRamAddress::Lint => 217, } } } fn command(operation: u8, address: u8, data: u8) -> [u8; 4] { [operation, address, data, !operation] }