Compare commits

...

2 commits

Author SHA1 Message Date
Alex Janka 5c8bad1d3b tokio-modbus instead of libmodbus-rs
Some checks failed
Build prerelease .deb / Build (push) Failing after 5s
2024-07-09 10:35:28 +10:00
Alex Janka e6c7e3fb7f better workflows 2024-07-09 10:35:28 +10:00
11 changed files with 297 additions and 565 deletions

View file

@ -0,0 +1,29 @@
name: Build .deb
on: workflow_call
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
jobs:
Build-Deb:
runs-on: aarch64
steps:
- name: Run sccache-cache
uses: https://github.com/mozilla-actions/sccache-action@v0.0.5
- name: Check out repository code
uses: actions/checkout@v3
with:
submodules: "recursive"
- name: Build
run: "cargo build --release --target=aarch64-unknown-linux-gnu"
- name: Build .deb
run: "cargo deb --target=aarch64-unknown-linux-gnu"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: release
path: |
./target/aarch64-unknown-linux-gnu/debian/*.deb
./target/aarch64-unknown-linux-gnu/release/tesla-charge-controller

View file

@ -1,25 +1,21 @@
name: Build .deb on release name: Build and release .deb
run-name: Building .deb of latest release and adding to apt repo
on: on:
push: push:
tags: tags:
- "*" - "v[0-9]+.[0-9]+.[0-9]+"
jobs: jobs:
Build-Deb: Build:
uses: ./.gitea/workflows/build.yaml
Release:
runs-on: aarch64 runs-on: aarch64
steps: steps:
- name: Check out repository code - name: Download artifacts
uses: actions/checkout@v3 uses: actions/download-artifact@v4
with:
submodules: "recursive"
- name: Build
run: "cargo build --release --target=aarch64-unknown-linux-gnu"
- name: Build .deb
run: "cargo deb --target=aarch64-unknown-linux-gnu"
- name: Add .deb to apt repository - name: Add .deb to apt repository
run: "curl --user alex:${{ secrets.PACKAGING_TOKEN }} --upload-file $(ls -t ./target/aarch64-unknown-linux-gnu/debian/*.deb | head -1) https://git.alexjanka.com/api/packages/alex/debian/pool/testing/main/upload" run: "curl --user alex:${{ secrets.PACKAGING_TOKEN }} --upload-file $(ls -t ./release/*.deb | head -1) https://git.alexjanka.com/api/packages/alex/debian/pool/testing/main/upload"
- name: "Release package" - name: "Release package"
id: use-go-action id: use-go-action
uses: https://gitea.com/actions/release-action@main uses: https://gitea.com/actions/release-action@main

10
.gitea/workflows/pre.yaml Normal file
View file

@ -0,0 +1,10 @@
name: Build prerelease .deb
on:
push:
tags:
- "*-pre"
jobs:
Build:
uses: ./.gitea/workflows/build.yaml

707
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "tesla-charge-controller" name = "tesla-charge-controller"
version = "1.4.0" version = "1.5.0-pre"
edition = "2021" edition = "2021"
license = "MITNFA" license = "MITNFA"
description = "Controls Tesla charge rate based on solar charge data" description = "Controls Tesla charge rate based on solar charge data"
@ -28,7 +28,8 @@ prometheus = "0.13"
env_logger = "0.10" env_logger = "0.10"
log = "0.4" log = "0.4"
serialport = "4.3" serialport = "4.3"
libmodbus-rs = "0.8.3" tokio-modbus = "0.13.1"
tokio-serial = "5.4.4"
if_chain = "1.0.2" if_chain = "1.0.2"
notify-debouncer-mini = { version = "0.4.1", default-features = false } notify-debouncer-mini = { version = "0.4.1", default-features = false }
lazy_static = "1.4" lazy_static = "1.4"

View file

@ -470,7 +470,9 @@ impl TeslaInterface {
.map(|v| { .map(|v| {
if self.monitored_values.conn_charge_cable != v.conn_charge_cable { if self.monitored_values.conn_charge_cable != v.conn_charge_cable {
log::warn!("Current conn charge cable: \"{}\"", v.conn_charge_cable); log::warn!("Current conn charge cable: \"{}\"", v.conn_charge_cable);
self.monitored_values.conn_charge_cable = v.conn_charge_cable.clone(); self.monitored_values
.conn_charge_cable
.clone_from(&v.conn_charge_cable);
} }
v.into() v.into()
}); });
@ -507,7 +509,9 @@ impl TeslaInterface {
.and_then(|v| { .and_then(|v| {
if self.monitored_values.climate_keeper != v.climate_keeper_mode { if self.monitored_values.climate_keeper != v.climate_keeper_mode {
log::warn!("Current climate keeper mode: \"{}\"", v.climate_keeper_mode); log::warn!("Current climate keeper mode: \"{}\"", v.climate_keeper_mode);
self.monitored_values.climate_keeper = v.climate_keeper_mode.clone(); self.monitored_values
.climate_keeper
.clone_from(&v.climate_keeper_mode);
} }
v.try_into().ok() v.try_into().ok()
}); });

View file

@ -1,5 +1,5 @@
use libmodbus_rs::{Modbus, ModbusClient, ModbusRTU};
use prometheus::core::{AtomicI64, GenericGauge}; use prometheus::core::{AtomicI64, GenericGauge};
use tokio_modbus::client::Reader;
use crate::{ use crate::{
charge_controllers::gauges::*, charge_controllers::gauges::*,
@ -44,7 +44,7 @@ impl Scaling {
pub struct Tristar { pub struct Tristar {
state: TristarState, state: TristarState,
port_name: String, port_name: String,
modbus: Modbus, modbus: tokio_modbus::client::Context,
data_in: [u16; RAM_ARRAY_SIZE], data_in: [u16; RAM_ARRAY_SIZE],
charge_state_gauges: ChargeStateGauges, charge_state_gauges: ChargeStateGauges,
consecutive_errors: usize, consecutive_errors: usize,
@ -233,16 +233,12 @@ impl ChargeStateGauges {
} }
} }
const MAX_CONSECUTIVE_ERRORS: usize = 5;
impl Tristar { impl Tristar {
pub fn new(serial_port: String, baud_rate: i32) -> Result<Self, TristarError> { pub fn new(serial_port: String, baud_rate: u32) -> Result<Self, TristarError> {
let parity = 'N'; let modbus_serial =
let data_bit = 8; tokio_serial::SerialStream::open(&tokio_serial::new(&serial_port, baud_rate))?;
let stop_bit = 2; let slave = tokio_modbus::Slave(DEVICE_ID);
let mut modbus = Modbus::new_rtu(&serial_port, baud_rate, parity, data_bit, stop_bit)?; let modbus = tokio_modbus::client::rtu::attach_slave(modbus_serial, slave);
modbus.set_slave(DEVICE_ID)?;
modbus.connect()?;
let charge_state_gauges = ChargeStateGauges::new(&serial_port); let charge_state_gauges = ChargeStateGauges::new(&serial_port);
Ok(Self { Ok(Self {
state: Default::default(), state: Default::default(),
@ -254,9 +250,10 @@ impl Tristar {
}) })
} }
pub fn refresh(&mut self) { pub async fn refresh(&mut self) {
if let Some(new_state) = self if let Some(new_state) = self
.get_data() .get_data()
.await
.some_or_print_with("reading tristar state") .some_or_print_with("reading tristar state")
.map(|scaling| TristarState::from_ram(scaling, &self.data_in)) .map(|scaling| TristarState::from_ram(scaling, &self.data_in))
{ {
@ -297,21 +294,24 @@ impl Tristar {
self.charge_state_gauges.set(new_state.charge_state); self.charge_state_gauges.set(new_state.charge_state);
self.state = new_state; self.state = new_state;
} else {
self.consecutive_errors += 1;
if self.consecutive_errors >= MAX_CONSECUTIVE_ERRORS {
self.modbus.close();
if let Err(e) = self.modbus.connect() {
log::error!("error reconnecting to modbus device: {e:?}");
}
}
} }
// else {
// self.consecutive_errors += 1;
// if self.consecutive_errors >= MAX_CONSECUTIVE_ERRORS {
// self.modbus.close();
// if let Err(e) = self.modbus.connect() {
// log::error!("error reconnecting to modbus device: {e:?}");
// }
// }
// }
} }
fn get_data(&mut self) -> Result<Scaling, TristarError> { async fn get_data(&mut self) -> Result<Scaling, TristarError> {
self.modbus let data = self
.read_registers(0x0000, RAM_DATA_SIZE + 1, &mut self.data_in)?; .modbus
let scaling = Scaling::from(&self.data_in); .read_holding_registers(0x0000, RAM_DATA_SIZE + 1)
.await??;
let scaling = Scaling::from(&data);
Ok(scaling) Ok(scaling)
} }
} }

View file

@ -157,7 +157,7 @@ pub enum ChargeControllerConfig {
}, },
Tristar { Tristar {
serial_port: String, serial_port: String,
baud_rate: i32, baud_rate: u32,
watch_interval_seconds: u64, watch_interval_seconds: u64,
}, },
} }

View file

@ -104,14 +104,12 @@ pub enum TeslaStateParseError {
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum TristarError { pub enum TristarError {
#[error("modbus error")] #[error(transparent)]
Modbus(libmodbus_rs::prelude::Error), Modbus(#[from] tokio_modbus::Error),
} #[error(transparent)]
ModbusException(#[from] tokio_modbus::Exception),
impl From<libmodbus_rs::prelude::Error> for TristarError { #[error(transparent)]
fn from(value: libmodbus_rs::prelude::Error) -> Self { Serial(#[from] tokio_serial::Error),
Self::Modbus(value)
}
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]

View file

@ -72,8 +72,7 @@ async fn main() {
let (api_requests, mut api_receiver) = tokio::sync::mpsc::unbounded_channel(); let (api_requests, mut api_receiver) = tokio::sync::mpsc::unbounded_channel();
// and to the pli thread // and to the pli thread
let (pli_requests, mut pli_receiver) = tokio::sync::mpsc::unbounded_channel(); let (pli_requests, mut pli_receiver) = tokio::sync::mpsc::unbounded_channel();
// and to the charge rate controller thread
let (tcrc_requests, mut tcrc_receiver) = tokio::sync::mpsc::unbounded_channel();
// try to spawn the pli loop // try to spawn the pli loop
let pli = { let pli = {
let config = access_config(); let config = access_config();
@ -146,7 +145,7 @@ async fn main() {
); );
loop { loop {
interval.tick().await; interval.tick().await;
tristar.refresh(); tristar.refresh().await;
} }
}) })
}), }),
@ -173,7 +172,7 @@ async fn main() {
pl_state, pl_state,
api_requests, api_requests,
pli_requests, pli_requests,
tcrc_requests, // tcrc_requests,
)); ));
if let Some((mut interface, mut tesla_charge_rate_controller)) = interface_and_tcrc { if let Some((mut interface, mut tesla_charge_rate_controller)) = interface_and_tcrc {
@ -222,10 +221,6 @@ async fn main() {
api_message = api_receiver.recv() => match api_message { api_message = api_receiver.recv() => match api_message {
Some(message) => interface.process_request(message).await, Some(message) => interface.process_request(message).await,
None => panic!("Tesla send channel dropped") None => panic!("Tesla send channel dropped")
},
tcrc_message = tcrc_receiver.recv() => match tcrc_message {
Some(message) => tesla_charge_rate_controller.process_request(message),
None => panic!("TCRC send channel dropped")
} }
} }
} }

View file

@ -16,7 +16,7 @@ use crate::{
charge_controllers::pl::{PlState, PliRequest, RegulatorState}, charge_controllers::pl::{PlState, PliRequest, RegulatorState},
config::{access_config, write_to_config}, config::{access_config, write_to_config},
errors::{PrintErrors, ServerError}, errors::{PrintErrors, ServerError},
tesla_charge_rate::{TcrcRequest, TcrcState}, tesla_charge_rate::TcrcState,
types::CarState, types::CarState,
}; };
@ -30,7 +30,6 @@ pub struct ServerState {
pub pl_state: Option<Arc<RwLock<PlState>>>, pub pl_state: Option<Arc<RwLock<PlState>>>,
pub api_requests: UnboundedSender<InterfaceRequest>, pub api_requests: UnboundedSender<InterfaceRequest>,
pub pli_requests: UnboundedSender<PliRequest>, pub pli_requests: UnboundedSender<PliRequest>,
pub tcrc_requests: UnboundedSender<TcrcRequest>,
waiting_for_auth: std::sync::Mutex<Option<(String, std::time::Instant)>>, waiting_for_auth: std::sync::Mutex<Option<(String, std::time::Instant)>>,
} }
@ -41,7 +40,6 @@ impl ServerState {
pl_state: Option<Arc<RwLock<PlState>>>, pl_state: Option<Arc<RwLock<PlState>>>,
api_requests: UnboundedSender<InterfaceRequest>, api_requests: UnboundedSender<InterfaceRequest>,
pli_requests: UnboundedSender<PliRequest>, pli_requests: UnboundedSender<PliRequest>,
tcrc_requests: UnboundedSender<TcrcRequest>,
) -> Self { ) -> Self {
Self { Self {
car_state, car_state,
@ -49,7 +47,7 @@ impl ServerState {
pl_state, pl_state,
api_requests, api_requests,
pli_requests, pli_requests,
tcrc_requests, // tcrc_requests,
waiting_for_auth: std::sync::Mutex::new(None), waiting_for_auth: std::sync::Mutex::new(None),
} }
} }