Updates for rust 1.62 and latest FW

This commit is contained in:
Ashcon Mohseninia 2022-08-01 16:08:19 +01:00
parent bdd173c2d6
commit ddcc4b76cb
20 changed files with 1278 additions and 403 deletions

View File

@ -6,19 +6,23 @@ resolver = "2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ecu_diagnostics="0.90.41"
#egui="0.15.0"
serde_json = { version = "1.0.79" }
serde = { version = "1.0.137", features = ["derive"] }
#ecu_diagnostics="0.90.41"
ecu_diagnostics = {path="../../ecu_diagnostics"}
image = "0.24.1"
#serialport = {git = "https://github.com/rnd-ash/serialport-rs"} # For manual USB connection
serial-rs = {git="https://github.com/rnd-ash/serial-rs"}
nfd="0.0.4"
egui_wgpu_backend = "0.17"
egui_winit_platform = "0.14.0"
pollster = "0.2.5"
egui = "0.17.0"
epi = "0.17.0"
eframe = {git = "https://github.com/emilk/egui", features=["wgpu"] }
wgpu = "0.12"
winit = "0.26.1"
egui-winit = "0.17.0"
egui-wgpu = "0.18.0"
egui-winit = "0.18.0"
modular-bitfield = "0.11.2"
static_assertions = "1.1.0"
static_assertions = "1.1.0"
env_logger="0.9.0"
[profile.release]
debug = true

View File

@ -2,11 +2,10 @@
extern crate static_assertions;
use std::iter;
use egui::FontDefinitions;
use epi::App;
use eframe::{NativeOptions, Renderer};
use ui::launcher::Launcher;
use window::MainWindow;
use egui_wgpu_backend::{RenderPass, ScreenDescriptor};
use egui_wgpu::renderer::{RenderPass, ScreenDescriptor};
use egui_winit_platform::{Platform, PlatformDescriptor};
use winit::event::Event::*;
use winit::event_loop::ControlFlow;
@ -19,20 +18,9 @@ mod window;
#[cfg(all(target_arch = "x86_64", target_os = "windows"))]
compile_error!("Windows can ONLY be built using the i686-pc-windows-msvc target!");
enum Event {
RequestRedraw
}
struct RepaintSignal(std::sync::Mutex<winit::event_loop::EventLoopProxy<Event>>);
impl epi::backend::RepaintSignal for RepaintSignal {
fn request_repaint(&self) {
self.0.lock().unwrap().send_event(Event::RequestRedraw).ok();
}
}
fn main() {
env_logger::init();
/*
let event_loop = winit::event_loop::EventLoop::with_user_event();
let window = winit::window::WindowBuilder::new()
.with_decorations(true)
@ -44,187 +32,15 @@ fn main() {
})
.build(&event_loop)
.unwrap();
*/
let mut app = window::MainWindow::new();
app.add_new_page(Box::new(Launcher::new()));
let mut native_options = NativeOptions::default();
#[cfg(windows)]
let instance = wgpu::Instance::new(wgpu::Backends::DX12 | wgpu::Backends::DX11 | wgpu::Backends::GL);
#[cfg(unix)]
let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY);
let surface = unsafe { instance.create_surface(&window) };
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})).or_else(|| {
pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})) }
).unwrap();
let (device, queue) = pollster::block_on(adapter.request_device(
&wgpu::DeviceDescriptor {
features: wgpu::Features::default(),
limits: wgpu::Limits::default(),
label: None,
},
None,
))
.unwrap();
let size = window.inner_size();
let surface_format = surface.get_preferred_format(&adapter).unwrap();
let mut surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width as u32,
height: size.height as u32,
present_mode: wgpu::PresentMode::Fifo,
};
surface.configure(&device, &surface_config);
let repaint_signal = std::sync::Arc::new(RepaintSignal(std::sync::Mutex::new(
event_loop.create_proxy(),
)));
// We use the egui_winit_platform crate as the platform.
let mut platform = Platform::new(PlatformDescriptor {
physical_width: size.width as u32,
physical_height: size.height as u32,
scale_factor: window.scale_factor(),
font_definitions: FontDefinitions::default(),
style: Default::default(),
});
let mut egui_rpass = RenderPass::new(&device, surface_format, 1);
let mut state = egui_winit::State::new(4096, &window);
let context = egui::Context::default();
let mut app = MainWindow::new();
app.add_new_page(Box::new(Launcher::new()));
event_loop.run(move|event, _, control_flow| {
match event {
RedrawRequested(..) => {
let output_frame = match surface.get_current_texture() {
Ok(frame) => frame,
Err(wgpu::SurfaceError::Outdated) => {
// This error occurs when the app is minimized on Windows.
// Silently return here to prevent spamming the console with:
// "The underlying surface has changed, and therefore the swap chain must be updated"
return;
}
Err(e) => {
eprintln!("Dropped frame with error: {}", e);
return;
}
};
let output_view = output_frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
// Begin to draw the UI frame.
let input = state.take_egui_input(&window);
context.begin_frame(input);
let app_output = epi::backend::AppOutput::default();
let frame = epi::Frame::new(epi::backend::FrameData {
info: epi::IntegrationInfo {
name: "egui_example",
web_info: None,
cpu_usage: None,
native_pixels_per_point: Some(window.scale_factor() as _),
prefer_dark_mode: None,
},
output: app_output,
repaint_signal: repaint_signal.clone(),
});
// Draw the demo application.
app.update(&context, &frame);
// End the UI frame. We could now handle the output and draw the UI with the backend.
let output = context.end_frame();
let paint_jobs = context.tessellate(output.shapes);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("encoder"),
});
// Upload all resources for the GPU.
let screen_descriptor = ScreenDescriptor {
physical_width: surface_config.width,
physical_height: surface_config.height,
scale_factor: window.scale_factor() as f32,
};
egui_rpass.add_textures(&device, &queue, &output.textures_delta).unwrap();
egui_rpass.remove_textures(output.textures_delta).unwrap();
egui_rpass.update_buffers(&device, &queue, &paint_jobs, &screen_descriptor);
// Record all render passes.
egui_rpass
.execute(
&mut encoder,
&output_view,
&paint_jobs,
&screen_descriptor,
Some(wgpu::Color::BLACK),
)
.unwrap();
// Submit the commands.
queue.submit(iter::once(encoder.finish()));
// Redraw egui
output_frame.present();
// Suppport reactive on windows only, but not on linux.
// if _output.needs_repaint {
// *control_flow = ControlFlow::Poll;
// } else {
// *control_flow = ControlFlow::Wait;
// }
}
MainEventsCleared | UserEvent(Event::RequestRedraw) => {
window.request_redraw();
}
WindowEvent { event, .. } => match event {
winit::event::WindowEvent::Resized(size) => {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows.
if size.width > 0 && size.height > 0 {
surface_config.width = size.width;
surface_config.height = size.height;
surface.configure(&device, &surface_config);
}
}
winit::event::WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
}
event => {
// Pass the winit events to the platform integration.
state.on_event(&context, &event);
}
},
_ => (),
}
});
/*
let mut app = MainWindow::new();
let mut native_options = eframe::NativeOptions::default();
/*
if let Ok(img) = image::load_from_memory_with_format(TRAY_ICON, ImageFormat::Png) {
native_options.icon_data = Some(IconData {
rgba: img.clone().into_bytes(),
width: img.width(),
height: img.height(),
})
{
native_options.renderer = Renderer::Wgpu;
}
*/
app.add_new_page(Box::new(Launcher::new()));
native_options.initial_window_size = Some(Vec2::new(1280.0, 720.0));
eframe::run_native(Box::new(app), native_options)
*/
eframe::run_native("Ultimate NAG52 config suite", native_options, Box::new(|cc| {
Box::new(app)
}));
}

View File

@ -1,9 +1,9 @@
use std::{sync::{Arc, Mutex}, borrow::BorrowMut};
use ecu_diagnostics::{hardware::Hardware, kwp2000::{Kwp2000DiagnosticServer, Kwp2000ServerOptions, Kwp2000VoidHandler, SessionType, ResetMode}, DiagnosticServer};
use egui::{Ui, Label};
use epi::Frame;
use crate::{usb_hw::diag_usb::Nag52USB, window::PageAction};
use ecu_diagnostics::{kwp2000::{Kwp2000DiagnosticServer, SessionType, ResetMode}, DiagnosticServer};
use eframe::egui::Ui;
use eframe::egui::{self, *};
use crate::window::PageAction;
use self::cfg_structs::{TcmCoreConfig, DefaultProfile, EngineType};
@ -31,7 +31,7 @@ impl ConfigPage {
impl crate::window::InterfacePage for ConfigPage {
fn make_ui(&mut self, ui: &mut Ui, frame: &Frame) -> PageAction {
fn make_ui(&mut self, ui: &mut Ui, frame: &eframe::Frame) -> PageAction {
ui.heading("TCM Configuration");

View File

@ -1,19 +1,15 @@
use std::{
fs::File,
io::{Read, Write},
io::Write,
sync::{
atomic::{AtomicBool, AtomicU32, Ordering},
Arc, Mutex, RwLock,
}, num::Wrapping, ops::DerefMut,
}, ops::DerefMut,
};
use ecu_diagnostics::{kwp2000::{Kwp2000DiagnosticServer, SessionType, ResetMode}, DiagServerResult, DiagnosticServer, DiagError};
use egui::*;
use epi::*;
use nfd::Response;
use ecu_diagnostics::{kwp2000::{Kwp2000DiagnosticServer, SessionType}, DiagServerResult, DiagnosticServer, DiagError};
use eframe::egui::{self, *};
use crate::{
usb_hw::{diag_usb::Nag52USB, flasher::{self, bin::{Firmware, load_binary}}},
window::{InterfacePage, PageAction},
};
@ -91,7 +87,7 @@ impl InterfacePage for CrashAnalyzerUI {
fn make_ui(
&mut self,
ui: &mut egui::Ui,
frame: &epi::Frame,
frame: &eframe::Frame,
) -> crate::window::PageAction {
ui.heading("Crash Analyzer");
ui.label(

View File

@ -1,23 +1,19 @@
use std::collections::VecDeque;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::mem::{size_of, transmute};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use ecu_diagnostics::hardware::Hardware;
use ecu_diagnostics::kwp2000::{Kwp2000DiagnosticServer, Kwp2000ServerOptions, Kwp2000VoidHandler};
use egui::plot::{Plot, Value, Line, Values, Legend};
use egui::{Ui, RichText, Color32};
use epi::Frame;
use std::time::Instant;
use ecu_diagnostics::kwp2000::Kwp2000DiagnosticServer;
use eframe::egui::plot::{Plot, Value, Line, Values, Legend};
use eframe::egui::{Ui, RichText, Color32};
use crate::ui::status_bar::MainStatusBar;
use crate::window::{PageAction, StatusBar};
pub mod rli;
pub mod data;
pub mod solenoids;
use crate::usb_hw::diag_usb::Nag52USB;
use ecu_diagnostics::{kwp2000::*, bcd_decode};
use crate::ui::diagnostics::rli::{DataCanDump, DataGearboxSensors, DataSolenoids, LocalRecordData, RecordIdents};
pub mod shift_reporter;
use crate::ui::diagnostics::rli::{LocalRecordData, RecordIdents};
use self::rli::ChartData;
@ -58,7 +54,8 @@ impl DiagnosticsPage {
impl crate::window::InterfacePage for DiagnosticsPage {
fn make_ui(&mut self, ui: &mut Ui, frame: &Frame) -> PageAction {
fn make_ui(&mut self, ui: &mut Ui, frame: &eframe::Frame) -> PageAction {
let mut pending = false;
ui.heading("This is experimental, use with MOST up-to-date firmware");
if ui.button("Query ECU Serial number").clicked() {
@ -100,12 +97,25 @@ impl crate::window::InterfacePage for DiagnosticsPage {
self.charting_data.clear();
self.record_data = None;
}
if ui.button("Query solenoid pressures").clicked() {
self.record_to_query = Some(RecordIdents::PressureStatus);
self.chart_idx = 0;
self.charting_data.clear();
self.record_data = None;
}
if ui.button("Query can Rx data").clicked() {
self.record_to_query = Some(RecordIdents::CanDataDump);
self.chart_idx = 0;
self.charting_data.clear();
self.record_data = None;
}
if ui.button("Query i2S DMA data").clicked() {
self.record_to_query = Some(RecordIdents::DmaDump);
self.chart_idx = 0;
self.charting_data.clear();
self.record_data = None;
pending = true;
}
if ui.button("Query Performance metrics").clicked() {
self.record_to_query = Some(RecordIdents::SysUsage);
@ -124,64 +134,87 @@ impl crate::window::InterfacePage for DiagnosticsPage {
},
}
if self.query_loop && self.last_query_time.elapsed().as_millis() > 100 {
if pending || (self.query_loop && self.last_query_time.elapsed().as_millis() > 100) {
self.last_query_time = Instant::now();
self.chart_idx += 100;
if let Some(rid) = self.record_to_query {
if let Ok(r) = rid.query_ecu(&mut self.server.lock().unwrap()) {
self.record_data = Some(r)
match rid.query_ecu(&mut self.server.lock().unwrap()) {
Ok(r) => self.record_data = Some(r),
Err(e) => {
eprintln!("Could not query {}", e);
}
}
}
}
if let Some(data) = &self.record_data {
data.to_table(ui);
let c = data.get_chart_data();
if !c.is_empty() {
let d = &c[0];
self.charting_data.push_back((
self.chart_idx,
d.clone()
));
if self.charting_data.len() > 500 {
let _ = self.charting_data.pop_front();
if let LocalRecordData::Dma(dma) = data {
let mut points: Vec<Value> = Vec::new();
for (idx, y) in dma.dma_buffer.clone().iter().enumerate() {
points.push(Value::new(idx as f64, *y));
}
let avg = dma.adc_detect;
Plot::new("I2S DMA")
.allow_drag(false)
.include_x(0)
.include_x(1000)
.include_y(3300)
.show(ui, |plot_ui| {
plot_ui.line(Line::new(Values::from_values(points)));
plot_ui.line(Line::new(Values::from_values(vec![Value::new(0, avg), Value::new(1000, avg) ])).highlight(true))
});
// Can guarantee everything in `self.charting_data` will have the SAME length
// as `d`
let mut lines = Vec::new();
let mut legend = Legend::default();
} else {
for (idx, (key, _, _)) in d.data.iter().enumerate() {
let mut points: Vec<Value> = Vec::new();
for (timestamp, point) in &self.charting_data {
points.push(Value::new(*timestamp as f64, point.data[idx].1))
let c = data.get_chart_data();
if !c.is_empty() {
let d = &c[0];
self.charting_data.push_back((
self.chart_idx,
d.clone()
));
if self.charting_data.len() > 1000 {
let _ = self.charting_data.pop_front();
}
let mut key_hasher = DefaultHasher::default();
key.hash(&mut key_hasher);
let r = key_hasher.finish();
lines.push(Line::new(Values::from_values(points)).name(key.clone()).color(Color32::from_rgb((r & 0xFF) as u8, ((r >> 8) & 0xFF) as u8, ((r >> 16) & 0xFF) as u8)))
// Can guarantee everything in `self.charting_data` will have the SAME length
// as `d`
let mut lines = Vec::new();
let mut legend = Legend::default();
for (idx, (key, _, _)) in d.data.iter().enumerate() {
let mut points: Vec<Value> = Vec::new();
for (timestamp, point) in &self.charting_data {
points.push(Value::new(*timestamp as f64, point.data[idx].1))
}
let mut key_hasher = DefaultHasher::default();
key.hash(&mut key_hasher);
let r = key_hasher.finish();
lines.push(Line::new(Values::from_values(points)).name(key.clone()).color(Color32::from_rgb((r & 0xFF) as u8, ((r >> 8) & 0xFF) as u8, ((r >> 16) & 0xFF) as u8)))
}
let mut plot = Plot::new(d.group_name.clone())
.allow_drag(false)
.legend(legend);
if let Some((min, max)) = &d.bounds {
plot = plot.include_y(*min);
if *max > 0.1 { // 0.0 check
plot = plot.include_y(*max);
}
}
plot.show(ui, |plot_ui| {
for x in lines {
plot_ui.line(x)
}
});
}
let mut plot = Plot::new(d.group_name.clone())
.allow_drag(false)
.legend(legend);
if let Some((min, max)) = &d.bounds {
plot = plot.include_y(*min);
if *max > 0.1 { // 0.0 check
plot = plot.include_y(*max);
}
}
plot.show(ui, |plot_ui| {
for x in lines {
plot_ui.line(x)
}
});
}
ui.ctx().request_repaint();
}
PageAction::None

View File

@ -1,10 +1,11 @@
//! Read data by local identifier data structures
//! Based on diag_data.h in TCM source code
//!
use std::borrow::Borrow;
use std::mem::size_of;
use ecu_diagnostics::{DiagError, DiagServerResult};
use ecu_diagnostics::kwp2000::{Kwp2000DiagnosticServer};
use egui::{Color32, InnerResponse, RichText, Ui};
use eframe::egui::{self, Color32, InnerResponse, RichText, Ui};
use modular_bitfield::{bitfield, BitfieldSpecifier};
#[repr(u8)]
@ -14,6 +15,8 @@ pub enum RecordIdents {
SolenoidStatus = 0x21,
CanDataDump = 0x22,
SysUsage = 0x23,
PressureStatus = 0x25,
DmaDump = 0x26,
}
impl RecordIdents {
@ -24,6 +27,14 @@ impl RecordIdents {
Self::SolenoidStatus => Ok(LocalRecordData::Solenoids(DataSolenoids::from_bytes(resp.try_into().map_err(|_| DiagError::InvalidResponseLength )?))),
Self::CanDataDump => Ok(LocalRecordData::Canbus(DataCanDump::from_bytes(resp.try_into().map_err(|_| DiagError::InvalidResponseLength )?))),
Self::SysUsage => Ok(LocalRecordData::SysUsage(DataSysUsage::from_bytes(resp.try_into().map_err(|_| DiagError::InvalidResponseLength )?))),
Self::PressureStatus => Ok(LocalRecordData::Pressures(DataPressures::from_bytes(resp.try_into().map_err(|_| DiagError::InvalidResponseLength )?))),
Self::DmaDump => {
let res = DataDmaDump {
adc_detect: u16::from_le_bytes([resp[0], resp[1]]),
dma_buffer: resp[2..].chunks_exact(2).into_iter().map(|x| u16::from_le_bytes([x[0], x[1]]) & 0x0FFF).collect(),
};
Ok(LocalRecordData::Dma(res))
}
}
}
}
@ -33,7 +44,9 @@ pub enum LocalRecordData {
Sensors(DataGearboxSensors),
Solenoids(DataSolenoids),
Canbus(DataCanDump),
SysUsage(DataSysUsage)
SysUsage(DataSysUsage),
Pressures(DataPressures),
Dma(DataDmaDump)
}
impl LocalRecordData {
@ -42,7 +55,12 @@ impl LocalRecordData {
LocalRecordData::Sensors(s) => s.to_table(ui),
LocalRecordData::Solenoids(s) => s.to_table(ui),
LocalRecordData::Canbus(s) => s.to_table(ui),
LocalRecordData::SysUsage(s) => s.to_table(ui)
LocalRecordData::SysUsage(s) => s.to_table(ui),
LocalRecordData::Pressures(s) => s.to_table(ui),
_ => {
egui::Grid::new("DGS").striped(true).show(ui, |ui| {
})
}
}
}
@ -52,11 +70,56 @@ impl LocalRecordData {
LocalRecordData::Solenoids(s) => s.to_chart_data(),
LocalRecordData::Canbus(s) => s.to_chart_data(),
LocalRecordData::SysUsage(s) => s.to_chart_data(),
LocalRecordData::Pressures(s) => s.to_chart_data(),
_ => vec![]
}
}
}
#[bitfield]
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct DataPressures {
pub spc_pwm: u16,
pub mpc_pwm: u16,
pub tcc_pwm: u16,
pub spc_pressure: u16,
pub mpc_pressure: u16,
pub tcc_pressure: u16
}
impl DataPressures {
pub fn to_table(&self, ui: &mut Ui) -> InnerResponse<()> {
egui::Grid::new("DGS").striped(true).show(ui, |ui| {
ui.label("Shift pressure");
ui.label(if self.spc_pressure() == u16::MAX { make_text("ERROR", true) } else { make_text(format!("{} mBar", self.spc_pressure()), false) });
ui.end_row();
ui.label("Modulating pressure");
ui.label(if self.mpc_pressure() == u16::MAX { make_text("ERROR", true) } else { make_text(format!("{} mBar", self.mpc_pressure()), false) });
ui.end_row();
ui.label("Torque converter pressure");
ui.label(if self.tcc_pressure() == u16::MAX { make_text("ERROR", true) } else { make_text(format!("{} mBar", self.tcc_pressure()), false) });
ui.end_row();
})
}
pub fn to_chart_data(&self) -> Vec<ChartData> {
vec![
ChartData::new(
"Requested pressures".into(),
vec![
("SPC pressure", self.spc_pressure() as f32, None),
("MPC pressure", self.mpc_pressure() as f32, None),
("TCC pressure", self.tcc_pressure() as f32, None),
],
Some((0.0, 0.0))
),
]
}
}
#[bitfield]
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct DataGearboxSensors {
@ -101,7 +164,7 @@ impl DataGearboxSensors {
ui.end_row();
ui.label("ATF Oil temperature\n(Only when parking lock off)");
ui.label(if self.atf_temp_c() as i32 == u16::MAX as i32 { make_text("Cannot read\nParking lock engaged", true) } else { make_text(format!("{} *C", self.atf_temp_c() as i32), false) });
ui.label(if self.parking_lock() != 0x00 { make_text("Cannot read\nParking lock engaged", true) } else { make_text(format!("{} *C", self.atf_temp_c() as i32), false) });
ui.end_row();
ui.label("Parking lock");
@ -139,6 +202,12 @@ impl ChartData {
}
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct DataDmaDump {
pub adc_detect: u16,
pub dma_buffer: Vec<u16>
}
#[bitfield]
#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct DataSolenoids {
@ -151,6 +220,10 @@ pub struct DataSolenoids {
pub spc_current: u16,
pub mpc_current: u16,
pub tcc_current: u16,
pub targ_spc_current: u16,
pub targ_mpc_current: u16,
pub adjustment_spc: u16,
pub adjustment_mpc: u16,
pub y3_current: u16,
pub y4_current: u16,
pub y5_current: u16
@ -160,11 +233,13 @@ impl DataSolenoids {
pub fn to_table(&self, ui: &mut Ui) -> InnerResponse<()> {
egui::Grid::new("DGS").striped(true).show(ui, |ui| {
ui.label("MPC Solenoid");
ui.label(format!("PWM {:>4}/4096, Est current {} mA", self.mpc_pwm(), self.mpc_current()));
ui.label(format!("PWM {:>4}/4096, Est current {} mA. Targ current {} mA. PWM Trim {:.2} %", self.mpc_pwm(), self.mpc_current(), self.targ_mpc_current(),
(self.adjustment_mpc() as f32 / 10.0) -100.0));
ui.end_row();
ui.label("SPC Solenoid");
ui.label(format!("PWM {:>4}/4096, Est current {} mA", self.spc_pwm(), self.spc_current()));
ui.label(format!("PWM {:>4}/4096, Est current {} mA. Targ current {} mA. PWM Trim {:.2} %", self.spc_pwm(), self.spc_current(), self.targ_spc_current(),
(self.adjustment_spc() as f32 / 10.0) -100.0));
ui.end_row();
ui.label("TCC Solenoid");
@ -185,12 +260,12 @@ impl DataSolenoids {
ui.label("Total current consumption");
ui.label(format!("{} mA",
self.y5_current() +
self.y4_current() +
self.y3_current() +
self.mpc_current() +
self.spc_current() +
self.tcc_current()
self.y5_current() as u32 +
self.y4_current() as u32 +
self.y3_current() as u32 +
self.mpc_current() as u32 +
self.spc_current() as u32 +
self.tcc_current() as u32
));
ui.end_row();
})
@ -250,6 +325,10 @@ pub enum ShifterPosition {
Drive,
Plus,
Minus,
Four,
Three,
Two,
One,
SNV = 0xFF
}
@ -339,14 +418,24 @@ impl DataCanDump {
pub struct DataSysUsage {
core1_usage: u16,
core2_usage: u16,
free_heap: u32,
free_ram: u32,
total_ram: u32,
free_psram: u32,
total_psram: u32,
num_tasks: u32,
}
impl DataSysUsage {
pub fn to_table(&self, ui: &mut Ui) -> InnerResponse<()> {
println!("{:#?}", self);
let r_f = self.free_ram() as f32;
let r_t = self.total_ram() as f32;
let p_f = self.free_psram() as f32;
let p_t = self.total_psram() as f32;
let used_ram_perc = 100f32 * (r_t-r_f) / r_t;
let used_psram_perc = 100f32 * (p_t-p_f) / p_t;
egui::Grid::new("DGS").striped(true).show(ui, |ui| {
ui.label("Core 1 usage");
ui.label(format!("{:.1} %", self.core1_usage() as f32 / 10.0));
@ -356,12 +445,16 @@ impl DataSysUsage {
ui.label(format!("{:.1} %", self.core2_usage() as f32 / 10.0));
ui.end_row();
ui.label("Free Heap");
ui.label(format!("{:.1} Kb", self.free_heap() as f32 / 1024.0));
ui.label("Free internal RAM");
ui.label(format!("{:.1} Kb ({:.1}% Used)", self.free_ram() as f32 / 1024.0, used_ram_perc));
ui.end_row();
ui.label("Free PSRAM");
ui.label(format!("{:.1} Kb", self.free_psram() as f32 / 1024.0));
ui.label(format!("{:.1} Kb ({:.1}% Used)", self.free_psram() as f32 / 1024.0, used_psram_perc));
ui.end_row();
ui.label("Num. OS Tasks");
ui.label(format!("{}", self.num_tasks()));
ui.end_row();
})
}

View File

@ -0,0 +1,460 @@
use std::{sync::{Arc, atomic::{AtomicBool, Ordering}, RwLock, Mutex}, thread, time::Duration};
use ecu_diagnostics::{kwp2000::{Kwp2000DiagnosticServer, SessionType, KWP2000Error}, DiagnosticServer};
use eframe::{egui::{plot::{Plot, Points, Value, Line, Values, LinkedAxisGroup, VLine, Text, LineStyle, PlotUi, Corner}, RichText}, epaint::{Stroke, Color32}};
use modular_bitfield::bitfield;
use serde::{Serialize, Deserialize};
use crate::{ui::status_bar::MainStatusBar, window::PageAction};
use crate::ui::egui::ComboBox;
// Data structure for shift report
pub const MAX_POINTS_PER_SR_ARRAY: usize = 6000/50;
pub const REPORT_LEN: usize = std::mem::size_of::<ShiftReport>();
#[repr(packed)]
#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Deserialize, Serialize)]
struct ShiftPhase {
ramp_time: u16,
hold_time: u16,
spc_pressure: u16,
mpc_pressure: u16
}
#[repr(packed)]
#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
struct ShiftReport {
atf_temp_c: i16,
targ_curr: u8,
profile: u8,
requested_torque: u16,
interval_points: u8,
report_len: u16,
engine_rpm: [u16; MAX_POINTS_PER_SR_ARRAY],
input_rpm: [u16; MAX_POINTS_PER_SR_ARRAY],
output_rpm: [u16; MAX_POINTS_PER_SR_ARRAY],
engine_torque: [i16; MAX_POINTS_PER_SR_ARRAY],
total_ms: u16,
initial_mpc_pressure: u16,
hold1_data: ShiftPhase,
hold2_data: ShiftPhase,
hold3_data: ShiftPhase,
torque_data: ShiftPhase,
overlap_data: ShiftPhase,
max_pressure_data: ShiftPhase,
transition_start: u16,
transition_end: u16,
flare_timestamp: u16,
timeout: u8,
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
struct SerializedShiftReport {
atf_temp_c: i16,
targ_curr: u8,
profile: u8,
requested_torque: u16,
interval_points: u8,
report_len: u16,
engine_rpm: Vec<u16>,
input_rpm: Vec<u16>,
output_rpm: Vec<u16>,
engine_torque: Vec<i16>,
total_ms: u16,
initial_mpc_pressure: u16,
hold1_data: ShiftPhase,
hold2_data: ShiftPhase,
hold3_data: ShiftPhase,
torque_data: ShiftPhase,
overlap_data: ShiftPhase,
max_pressure_data: ShiftPhase,
transition_start: u16,
transition_end: u16,
flare_timestamp: u16,
timeout: u8,
}
impl SerializedShiftReport {
fn from_raw(rpt: &ShiftReport) -> Self {
let mut r = Self {
atf_temp_c: rpt.atf_temp_c,
targ_curr: rpt.targ_curr,
profile: rpt.profile,
requested_torque: rpt.requested_torque,
interval_points: rpt.interval_points,
report_len: rpt.report_len,
engine_rpm: (&{rpt.engine_rpm}).to_vec(),
input_rpm: (&{rpt.input_rpm}).to_vec(),
output_rpm: (&{rpt.output_rpm}).to_vec(),
engine_torque: (&{rpt.engine_torque}).to_vec(),
total_ms: rpt.total_ms,
initial_mpc_pressure: rpt.initial_mpc_pressure,
hold1_data: rpt.hold1_data,
hold2_data: rpt.hold2_data,
hold3_data: rpt.hold3_data,
torque_data: rpt.torque_data,
overlap_data: rpt.overlap_data,
max_pressure_data: rpt.max_pressure_data,
transition_start: rpt.transition_start,
transition_end: rpt.transition_end,
flare_timestamp: rpt.flare_timestamp,
timeout: rpt.timeout,
};
r.engine_rpm.resize(r.report_len as usize, 0);
r.input_rpm.resize(r.report_len as usize, 0);
r.output_rpm.resize(r.report_len as usize, 0);
r.engine_torque.resize(r.report_len as usize, 0);
r
}
fn to_raw(&self) -> ShiftReport {
let mut r = ShiftReport {
atf_temp_c: self.atf_temp_c,
targ_curr: self.targ_curr,
profile: self.targ_curr,
requested_torque: self.requested_torque,
interval_points: self.interval_points,
report_len: self.report_len,
engine_rpm: [0x00; MAX_POINTS_PER_SR_ARRAY],
input_rpm: [0x00; MAX_POINTS_PER_SR_ARRAY],
output_rpm: [0x00; MAX_POINTS_PER_SR_ARRAY],
engine_torque: [0x00; MAX_POINTS_PER_SR_ARRAY],
total_ms: self.total_ms,
initial_mpc_pressure: self.initial_mpc_pressure,
hold1_data: self.hold1_data,
hold2_data: self.hold2_data,
hold3_data: self.hold3_data,
torque_data: self.torque_data,
overlap_data: self.overlap_data,
max_pressure_data: self.max_pressure_data,
transition_start: self.transition_start,
transition_end: self.transition_end,
flare_timestamp: self.flare_timestamp,
timeout: self.timeout,
};
unsafe {
//r.engine_rpm
//r.engine_rpm.copy_from_slice(&self.engine_rpm);
//r.input_rpm.copy_from_slice(&self.input_rpm);
//r.output_rpm.copy_from_slice(&self.output_rpm);
//r.engine_torque.copy_from_slice(&self.engine_torque);
}
r
}
}
pub struct ShiftReportPage{
bar: MainStatusBar,
curr_report: Option<ShiftReport>,
server: Arc<Mutex<Kwp2000DiagnosticServer>>,
report_list: Vec<(u8, i32, u8, u8)>, // ID, ATF Temp, curr, target
err: Option<String>,
select_id: u32,
axis_group: LinkedAxisGroup
}
impl ShiftReportPage {
pub fn new(server: Arc<Mutex<Kwp2000DiagnosticServer>>, bar: MainStatusBar) -> Self {
server.lock().unwrap().set_diagnostic_session_mode(SessionType::ExtendedDiagnostics);
Self {
bar,
curr_report: None,
report_list: Vec::new(),
err: None,
server,
select_id: 0,
axis_group: LinkedAxisGroup::x()
}
}
pub fn parse_error(&mut self, e: ecu_diagnostics::DiagError) {
match e {
ecu_diagnostics::DiagError::ECUError { code, def: _ } => {
// Conditions not correct actually implies in this mode that car is changing gear
// so we don't have a lock on the resource
if code == 0x22 {
self.err = Some(format!("Car is currently changing gear, cannot query"))
} else {
self.err = Some(format!("Error ECU rejected the request: {}", e))
}
},
_ => self.err = Some(format!("Error querying ECU: {}", e))
}
}
}
impl crate::window::InterfacePage for ShiftReportPage {
fn make_ui(&mut self, ui: &mut eframe::egui::Ui, frame: &eframe::Frame) -> crate::window::PageAction {
ui.heading("Shift report history");
if ui.button("Query a list of available shifts").clicked() {
let rpt_query = self.server.lock().unwrap().send_byte_array_with_response(&[0x88, 0x00, 0x00]);
match rpt_query {
Ok(mut res) => {
res.drain(0..1);
if res.len() % 4 != 0 {
self.err = Some(format!("Incorrect report length!"));
return PageAction::None
}
self.report_list.clear();
for chunk in res.chunks(4) {
let id = chunk[0];
let targ = (chunk[1] & 0xF0) >> 4;
let curr = chunk[1] & 0x0F;
let atf: i16 = (chunk[2] as i16) << 8 | chunk[3] as i16;
if targ != 0 && curr != 0 && atf != 0 {
self.report_list.push((id, atf as i32, targ, curr))
}
}
},
Err(e) => self.parse_error(e)
}
}
if self.report_list.is_empty() {
ui.label("No shift data available");
} else {
ComboBox::from_label("Select shift record")
.width(400.0)
.selected_text(format!("Shift ##{}", self.select_id))
.show_ui(ui, |cb_ui| {
for shift in &self.report_list {
cb_ui.selectable_value(&mut self.select_id, shift.0 as u32, format!("Shift ##{}", shift.0));
}
});
if ui.button("Query shift").clicked() {
let rpt_query = self.server.lock().unwrap().send_byte_array_with_response(&[0x88, 0x01, self.select_id as u8]);
match rpt_query {
Ok(mut res) => {
res.drain(0..1);
if res.len() != REPORT_LEN {
self.err = Some(format!("Incorrect report length. Want {} bytes, got {} bytes!", REPORT_LEN, res.len()));
return PageAction::None
}
unsafe {
let rpt_ptr: *const ShiftReport = res.as_ptr() as *const ShiftReport;
let mut rpt = *rpt_ptr;
rpt.transition_end;
rpt.transition_start;
self.curr_report = Some(rpt)
}
let x = self.curr_report.clone().unwrap();
println!("{} {} {}", &{x.total_ms}, &{x.transition_start}, &{x.transition_end});
},
Err(e) => self.parse_error(e)
}
}
}
if let Some(e) = &self.err {
ui.label(e);
}
if let Some(report) = &self.curr_report {
ui.heading("Shift stats:");
ui.label(format!("Gear {} to gear {}", report.targ_curr & 0xF, (report.targ_curr & 0xF0) >> 4));
ui.label(format!("ATF Temp: {}°C", &{report.atf_temp_c}));
ui.label(format!("Profile: {}", match report.profile { // Profiles.h
0 => "Standard",
1 => "Comfort",
2 => "Winter",
3 => "Agility",
4 => "Manual",
_ => "Unknown"
}));
ui.label(
if report.timeout == 0 {
RichText::new(format!("Shift completed after {} ms", &{report.total_ms}))
} else {
RichText::new(format!("Shift timed out after {} ms!", &{report.total_ms})).color(Color32::from_rgb(255, 0, 0))
}
);
if report.timeout == 0 {
ui.label(format!("Gear transition period: {} ms", report.transition_end - report.transition_start));
}
let time_axis: Vec<u16> = (0..=report.total_ms).step_by(report.interval_points as usize).collect();
let mut time = 0;
// Add pressure line (Static)
let mut pressure_spc_points: Vec<Value> = Vec::new();
let mut pressure_mpc_points: Vec<Value> = Vec::new();
let mut engine_rpm_points: Vec<Value> = Vec::new();
let mut input_rpm_points: Vec<Value> = Vec::new();
let mut output_rpm_points: Vec<Value> = Vec::new();
let mut torque_points: Vec<Value> = Vec::new();
pressure_spc_points.push(Value::new(0, 0)); // Always
pressure_mpc_points.push(Value::new(0, report.initial_mpc_pressure));
time += report.hold1_data.ramp_time;
pressure_spc_points.push(Value::new(time, report.hold1_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.hold1_data.mpc_pressure));
time += report.hold1_data.hold_time;
pressure_spc_points.push(Value::new(time, report.hold1_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.hold1_data.mpc_pressure));
time += report.hold2_data.ramp_time;
pressure_spc_points.push(Value::new(time, report.hold2_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.hold2_data.mpc_pressure));
time += report.hold2_data.hold_time;
pressure_spc_points.push(Value::new(time, report.hold2_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.hold2_data.mpc_pressure));
time += report.hold3_data.ramp_time;
pressure_spc_points.push(Value::new(time, report.hold3_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.hold3_data.mpc_pressure));
time += report.hold3_data.hold_time;
pressure_spc_points.push(Value::new(time, report.hold3_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.hold3_data.mpc_pressure));
time += report.torque_data.ramp_time;
pressure_spc_points.push(Value::new(time, report.torque_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.torque_data.mpc_pressure));
time += report.torque_data.hold_time;
pressure_spc_points.push(Value::new(time, report.torque_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.torque_data.mpc_pressure));
time += report.overlap_data.ramp_time;
pressure_spc_points.push(Value::new(time, report.overlap_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.overlap_data.mpc_pressure));
time += report.overlap_data.hold_time;
pressure_spc_points.push(Value::new(time, report.overlap_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.overlap_data.mpc_pressure));
time += report.max_pressure_data.ramp_time;
pressure_spc_points.push(Value::new(time, report.max_pressure_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.max_pressure_data.mpc_pressure));
time = report.total_ms;
pressure_spc_points.push(Value::new(time, report.max_pressure_data.spc_pressure));
pressure_mpc_points.push(Value::new(time, report.max_pressure_data.mpc_pressure));
let mut rpm_max = *std::cmp::max(unsafe { report.engine_rpm }.iter().max().unwrap(), unsafe { report.input_rpm }.iter().max().unwrap()) as f64;
rpm_max = std::cmp::max(rpm_max as u16, *unsafe { report.output_rpm }.iter().max().unwrap()) as f64;
let trq_max = *unsafe { report.engine_torque}.iter().max().unwrap() as f64;
for x in 0..report.report_len as usize {
engine_rpm_points.push(Value::new(time_axis[x], report.engine_rpm[x]));
input_rpm_points.push(Value::new(time_axis[x], report.input_rpm[x]));
output_rpm_points.push(Value::new(time_axis[x], report.output_rpm[x]));
torque_points.push(Value::new(time_axis[x], report.engine_torque[x]));
}
// Add phase indication lines
let spc_pressure_line = Line::new(Values::from_values(pressure_spc_points)).name("SPC Pressure (mBar)");
let mpc_pressure_line = Line::new(Values::from_values(pressure_mpc_points)).name("MPC Pressure (mBar)");
let engine_line = Line::new(Values::from_values(engine_rpm_points)).name("Engine (RPM)");
let output_line = Line::new(Values::from_values(output_rpm_points)).name("Output shaft (RPM)");
let input_line = Line::new(Values::from_values(input_rpm_points)).name("Input shaft (RPM)");
let torque_line = Line::new(Values::from_values(torque_points)).name("Engine torque (Nm)");
time = 0;
time += report.hold1_data.hold_time+report.hold1_data.ramp_time;
let hold1_end_time = time;
time += report.hold2_data.hold_time+report.hold2_data.ramp_time;
let hold2_end_time = time;
time += report.hold3_data.hold_time+report.hold3_data.ramp_time;
let hold3_end_time = time;
time += report.torque_data.hold_time+report.torque_data.ramp_time;
let torque_end_time = time;
time += report.overlap_data.hold_time+report.overlap_data.ramp_time;
let overlap_end_time = time;
time += report.max_pressure_data.hold_time+report.max_pressure_data.ramp_time;
let max_p_end_time = time;
// #27ae60
let phase_colour = Color32::from_rgb(0x27, 0xae, 0x60);
let ok_colour = Color32::from_rgb(0, 255, 0);
let timeout_colour = Color32::from_rgb(255, 0, 0);
let height_per_chart = (ui.available_height()-50.0)/3.0;
let legand = eframe::egui::plot::Legend::default().position(Corner::RightBottom);
let timeout = report.timeout != 0;
let add_shift_regions = |plot_ui: &mut PlotUi| {
plot_ui.vline(VLine::new(hold1_end_time).style(LineStyle::dashed_loose()).color(phase_colour));
plot_ui.vline(VLine::new(hold2_end_time).style(LineStyle::dashed_loose()).color(phase_colour));
plot_ui.vline(VLine::new(hold3_end_time).style(LineStyle::dashed_loose()).color(phase_colour));
plot_ui.vline(VLine::new(torque_end_time).style(LineStyle::dashed_loose()).color(phase_colour));
plot_ui.vline(VLine::new(overlap_end_time).style(LineStyle::dashed_loose()).color(phase_colour));
plot_ui.vline(VLine::new(max_p_end_time).style(LineStyle::dashed_loose()).color(phase_colour));
plot_ui.vline(VLine::new(report.total_ms).stroke(Stroke::new(2.0, if timeout {timeout_colour} else {ok_colour})));
};
let mut plot_pressure = Plot::new("SPC pressure")
.legend(legand.clone())
.height(height_per_chart)
.allow_drag(false)
.include_x(0)
.include_x(report.total_ms as f32 * 1.2)
.include_y(0)
.include_y(8000)
.link_axis(self.axis_group.clone())
.show(ui, |plot_ui| {
plot_ui.line(spc_pressure_line);
plot_ui.line(mpc_pressure_line);
add_shift_regions(plot_ui);
plot_ui.text(Text::new(Value::new((0+hold1_end_time)/2, 7700), "Bleed"));
plot_ui.text(Text::new(Value::new((hold1_end_time+hold2_end_time)/2, 7700), "Fill"));
plot_ui.text(Text::new(Value::new((hold2_end_time+hold3_end_time)/2, 7700), "Lock"));
plot_ui.text(Text::new(Value::new((hold3_end_time+torque_end_time)/2, 7700), "Torque"));
plot_ui.text(Text::new(Value::new((torque_end_time+overlap_end_time)/2, 7700), "Overlap"));
plot_ui.text(Text::new(Value::new((overlap_end_time+max_p_end_time)/2, 7700), "Max P"));
});
let mut plot_rpm = Plot::new("Input/Engine RPM")
.legend(legand.clone())
.height(height_per_chart)
.allow_drag(false)
.include_x(0)
.include_x(report.total_ms as f32 * 1.2)
.include_y(rpm_max * 1.2)
.link_axis(self.axis_group.clone())
.show(ui, |plot_ui| {
plot_ui.line(engine_line);
plot_ui.line(input_line);
plot_ui.line(output_line);
// Show the User where the gear shift occurred
if report.timeout == 0 {
plot_ui.vline(VLine::new(report.transition_start).style(LineStyle::dashed_loose()).color(Color32::from_rgb(255, 192, 203)));
plot_ui.vline(VLine::new(report.transition_end).style(LineStyle::dashed_loose()).color(Color32::from_rgb(255, 192, 203)));
plot_ui.text(Text::new(Value::new((report.transition_start+report.transition_end)/2, rpm_max * 0.9), "SHIFT"));
}
add_shift_regions(plot_ui)
});
let mut plot_torque = Plot::new("Engine torque")
.legend(legand)
.height(height_per_chart)
.allow_drag(false)
.include_x(0)
.include_x(report.total_ms as f32 * 1.2)
.include_y(trq_max * 1.2)
.link_axis(self.axis_group.clone())
.show(ui, |plot_ui| {
plot_ui.line(torque_line);
add_shift_regions(plot_ui);
});
}
PageAction::None
}
fn get_title(&self) -> &'static str {
"Shift report history"
}
fn get_status_bar(&self) -> Option<Box<dyn crate::window::StatusBar>> {
Some(Box::new(self.bar.clone()))
}
}

View File

@ -1,17 +1,21 @@
use std::{sync::{Arc, Mutex, atomic::{AtomicBool, Ordering}, RwLock}, thread, time::Duration, char::MAX};
use std::{sync::{Arc, Mutex, atomic::{AtomicBool, Ordering, AtomicU64}, RwLock}, thread, time::{Duration, Instant}, char::MAX};
use ecu_diagnostics::kwp2000::{Kwp2000DiagnosticServer, SessionType};
use egui::plot::{Plot, Legend, Line, Values, Value};
use eframe::egui::plot::{Plot, Legend, Line, Values, Value};
use crate::{ui::status_bar::MainStatusBar, window::PageAction};
use super::rli::{DataSolenoids, RecordIdents, LocalRecordData};
const UPDATE_DELAY_MS: u64 = 100;
pub struct SolenoidPage{
bar: MainStatusBar,
query_ecu: Arc<AtomicBool>,
curr_values: Arc<RwLock<Option<DataSolenoids>>>
last_update_time: Arc<AtomicU64>,
curr_values: Arc<RwLock<Option<DataSolenoids>>>,
prev_values: Arc<RwLock<Option<DataSolenoids>>>,
time_since_launch: Instant
}
impl SolenoidPage {
@ -22,22 +26,42 @@ impl SolenoidPage {
let store = Arc::new(RwLock::new(None));
let store_t = store.clone();
let store_old = Arc::new(RwLock::new(None));
let store_old_t = store_old.clone();
let launch_time = Instant::now();
let launch_time_t = launch_time.clone();
let last_update = Arc::new(AtomicU64::new(0));
let last_update_t = last_update.clone();
thread::spawn(move|| {
let _ = server.lock().unwrap().set_diagnostic_session_mode(SessionType::Normal);
while run_t.load(Ordering::Relaxed) {
let start = Instant::now();
if let Ok(r) = RecordIdents::SolenoidStatus.query_ecu(&mut *server.lock().unwrap()) {
if let LocalRecordData::Solenoids(s) = r {
let curr = *store_t.read().unwrap();
*store_old_t.write().unwrap() = curr;
*store_t.write().unwrap() = Some(s);
last_update_t.store(launch_time_t.elapsed().as_millis() as u64, Ordering::Relaxed);
}
}
std::thread::sleep(Duration::from_millis(100));
let taken = start.elapsed().as_millis() as u64;
if taken < UPDATE_DELAY_MS {
std::thread::sleep(Duration::from_millis(UPDATE_DELAY_MS-taken));
}
}
});
Self {
bar,
query_ecu: run,
curr_values: store
curr_values: store,
last_update_time: last_update,
prev_values: store_old,
time_since_launch: launch_time
}
}
}
@ -48,7 +72,7 @@ const MAX_DUTY: u16 = 0xFFF; // 12bit pwm (4096)
const VOLTAGE_HIGH: u16 = 12;
const VOLTAGE_LOW: u16 = 0;
fn make_line_duty_pwm(duty: u16, freq: u16, x_off: f64, y_off: f64) -> Values {
fn make_line_duty_pwm(duty: f32, freq: u16, x_off: f64, y_off: f64) -> Values {
let num_pulses = freq / GRAPH_TIME_MS as u16;
let pulse_width = GRAPH_TIME_MS as f32 / num_pulses as f32;
let pulse_on_width = (duty as f32/4096f32) * pulse_width;
@ -58,10 +82,10 @@ fn make_line_duty_pwm(duty: u16, freq: u16, x_off: f64, y_off: f64) -> Values {
let mut curr_x_pos = 0f32;
// Shortcut
if duty == MAX_DUTY {
if duty as u16 == MAX_DUTY {
points.push(Value::new(0, VOLTAGE_LOW));
points.push(Value::new(GRAPH_TIME_MS, VOLTAGE_LOW));
} else if duty == 0 {
} else if duty as u16 == 0 {
points.push(Value::new(0, VOLTAGE_HIGH));
points.push(Value::new(GRAPH_TIME_MS, VOLTAGE_HIGH));
} else {
@ -88,22 +112,35 @@ fn make_line_duty_pwm(duty: u16, freq: u16, x_off: f64, y_off: f64) -> Values {
impl crate::window::InterfacePage for SolenoidPage {
fn make_ui(&mut self, ui: &mut egui::Ui, frame: &epi::Frame) -> crate::window::PageAction {
fn make_ui(&mut self, ui: &mut eframe::egui::Ui, frame: &eframe::Frame) -> crate::window::PageAction {
ui.heading("Solenoid live view");
let curr = self.curr_values.read().unwrap().clone().unwrap_or_default();
let mut curr = self.curr_values.read().unwrap().clone().unwrap_or_default();
let mut prev = self.prev_values.read().unwrap().clone().unwrap_or_default();
let ms_since_update = std::cmp::min(UPDATE_DELAY_MS, self.time_since_launch.elapsed().as_millis() as u64 - self.last_update_time.load(Ordering::Relaxed));
let mut proportion_curr: f32 = (ms_since_update as f32)/UPDATE_DELAY_MS as f32; // Percentage of old value to use
let mut proportion_prev: f32 = 1.0 - proportion_curr; // Percentage of curr value to use
if ms_since_update == 0 {
proportion_prev = 0.5;
proportion_curr = 0.5;
} else if ms_since_update == UPDATE_DELAY_MS {
proportion_prev = 0.5;
proportion_curr = 0.5;
}
let mut lines = Vec::new();
let mut legend = Legend::default();
let c_height = (ui.available_height()-50.0)/6.0;
let c_height = ui.available_height()/6.0;
lines.push(("MPC", Line::new(make_line_duty_pwm((curr.mpc_pwm() as f32 * proportion_curr) + (prev.mpc_pwm() as f32 * proportion_prev), 1000, 0.0, 0.0)).name("MPC").width(2.0)));
lines.push(("SPC", Line::new(make_line_duty_pwm((curr.spc_pwm() as f32 * proportion_curr) + (prev.spc_pwm() as f32 * proportion_prev), 1000, 0.0, 0.0)).name("SPC").width(2.0)));
lines.push(("TCC", Line::new(make_line_duty_pwm((curr.tcc_pwm() as f32 * proportion_curr) + (prev.tcc_pwm() as f32 * proportion_prev), 1000, 0.0, 0.0)).name("TCC").width(2.0)));
lines.push(("MPC", Line::new(make_line_duty_pwm(curr.mpc_pwm(), 1000, 0.0, 0.0)).name("MPC").width(2.0)));
lines.push(("SPC", Line::new(make_line_duty_pwm(curr.spc_pwm(), 1000, 0.0, 0.0)).name("SPC").width(2.0)));
lines.push(("TCC", Line::new(make_line_duty_pwm(curr.tcc_pwm(), 100, 0.0, 0.0)).name("TCC").width(2.0)));
lines.push(("Y3", Line::new(make_line_duty_pwm(curr.y3_pwm(), 1000, 0.0, 0.0)).name("Y3").width(2.0)));
lines.push(("Y4", Line::new(make_line_duty_pwm(curr.y4_pwm(), 1000, 0.0, 0.0)).name("Y4").width(2.0)));
lines.push(("Y5", Line::new(make_line_duty_pwm(curr.y5_pwm(), 1000, 0.0, 0.0)).name("Y5").width(2.0)));
lines.push(("Y3", Line::new(make_line_duty_pwm((curr.y3_pwm() as f32 * proportion_curr) + (prev.y3_pwm() as f32 * proportion_prev), 1000, 0.0, 0.0)).name("Y3").width(2.0)));
lines.push(("Y4", Line::new(make_line_duty_pwm((curr.y4_pwm() as f32 * proportion_curr) + (prev.y4_pwm() as f32 * proportion_prev), 1000, 0.0, 0.0)).name("Y4").width(2.0)));
lines.push(("Y5", Line::new(make_line_duty_pwm((curr.y5_pwm() as f32 * proportion_curr) + (prev.y5_pwm() as f32 * proportion_prev), 1000, 0.0, 0.0)).name("Y5").width(2.0)));
for line in lines {
let mut plot = Plot::new(format!("Solenoid {}", line.0))
@ -119,9 +156,7 @@ impl crate::window::InterfacePage for SolenoidPage {
plot_ui.line(line.1)
});
}
ui.ctx().request_repaint();
PageAction::None
}
@ -138,4 +173,4 @@ impl Drop for SolenoidPage {
fn drop(&mut self) {
self.query_ecu.store(false, Ordering::Relaxed);
}
}
}

View File

@ -1,19 +1,16 @@
use std::{
fs::File,
io::Read,
sync::{
atomic::{AtomicBool, AtomicU32, Ordering},
Arc, Mutex, RwLock,
}, num::Wrapping, ops::DerefMut, time::Instant,
}, ops::DerefMut, time::Instant,
};
use ecu_diagnostics::{kwp2000::{Kwp2000DiagnosticServer, SessionType, ResetMode}, DiagServerResult, DiagnosticServer, DiagError};
use egui::*;
use epi::*;
use eframe::egui::*;
use eframe::egui;
use nfd::Response;
use crate::{
usb_hw::{diag_usb::Nag52USB, flasher::{self, bin::{Firmware, load_binary}}},
usb_hw::flasher::{bin::{Firmware, load_binary}},
window::{InterfacePage, PageAction},
};
@ -95,7 +92,7 @@ impl InterfacePage for FwUpdateUI {
fn make_ui(
&mut self,
ui: &mut egui::Ui,
frame: &epi::Frame,
frame: &eframe::Frame,
) -> crate::window::PageAction {
ui.heading("Firmware update");
ui.label(
@ -137,11 +134,16 @@ firmware.header.get_date()
let c = self.server.clone();
let state_c = self.flash_state.clone();
let fw = firmware.clone();
let mut old_timeouts = (0, 0);
std::thread::spawn(move|| {
let mut lock = c.lock().unwrap();
*state_c.write().unwrap() = FlashState::Prepare;
old_timeouts = (lock.get_read_timeout(), lock.get_write_timeout());
lock.set_rw_timeout(10000, 10000);
match init_flash_mode(&mut lock.deref_mut(), fw.raw.len() as u32) {
Err(e) => {
lock.set_rw_timeout(old_timeouts.0, old_timeouts.1);
*state_c.write().unwrap() = FlashState::Aborted(format!("ECU rejected flash programming mode: {}", e))
},
Ok(size) => {
@ -156,6 +158,7 @@ firmware.header.get_date()
if let Err(e) = lock.send_byte_array_with_response(&req) {
*state_c.write().unwrap() = FlashState::Aborted(format!("ECU failed to write data to flash: {}", e));
eprintln!("Writing failed! Error {}", e);
lock.set_rw_timeout(old_timeouts.0, old_timeouts.1);
failure = true;
break;
} else {
@ -166,11 +169,13 @@ firmware.header.get_date()
if !failure {
*state_c.write().unwrap() = FlashState::Verify;
if let Err(e) = on_flash_end(&mut lock) {
lock.set_rw_timeout(old_timeouts.0, old_timeouts.1);
*state_c.write().unwrap() = FlashState::Aborted(format!("ECU failed to verify flash: {}", e))
} else {
*state_c.write().unwrap() = FlashState::Completed;
}
}
lock.set_rw_timeout(old_timeouts.0, old_timeouts.1);
}
}
});

View File

@ -0,0 +1,123 @@
use std::{sync::{Arc, Mutex, atomic::{AtomicBool, Ordering, AtomicU64}, RwLock}, thread, time::{Duration, Instant}, char::MAX};
use ecu_diagnostics::kwp2000::{Kwp2000DiagnosticServer, SessionType};
use eframe::egui::plot::{Plot, Legend, Line, Values, Value};
use crate::{ui::status_bar::MainStatusBar, window::PageAction};
use rli::{DataSolenoids, RecordIdents, LocalRecordData};
use super::diagnostics::rli;
const UPDATE_DELAY_MS: u64 = 500;
pub struct IoManipulatorPage {
bar: MainStatusBar,
query_ecu: Arc<AtomicBool>,
curr_solenoid_values: Arc<RwLock<Option<DataSolenoids>>>,
time_since_launch: Instant,
show_ui: bool
}
impl IoManipulatorPage {
pub fn new(server: Arc<Mutex<Kwp2000DiagnosticServer>>, bar: MainStatusBar) -> Self {
let run = Arc::new(AtomicBool::new(true));
let run_t = run.clone();
let store = Arc::new(RwLock::new(None));
let store_t = store.clone();
let store_old = Arc::new(RwLock::new(None));
let store_old_t = store_old.clone();
let launch_time = Instant::now();
let launch_time_t = launch_time.clone();
let last_update = Arc::new(AtomicU64::new(0));
let last_update_t = last_update.clone();
thread::spawn(move|| {
let _ = server.lock().unwrap().set_diagnostic_session_mode(SessionType::Normal);
while run_t.load(Ordering::Relaxed) {
let start = Instant::now();
if let Ok(r) = RecordIdents::SolenoidStatus.query_ecu(&mut *server.lock().unwrap()) {
if let LocalRecordData::Solenoids(s) = r {
let curr = *store_t.read().unwrap();
*store_old_t.write().unwrap() = curr;
*store_t.write().unwrap() = Some(s);
last_update_t.store(launch_time_t.elapsed().as_millis() as u64, Ordering::Relaxed);
}
}
let taken = start.elapsed().as_millis() as u64;
if taken < UPDATE_DELAY_MS {
std::thread::sleep(Duration::from_millis(UPDATE_DELAY_MS-taken));
}
}
});
Self {
bar,
query_ecu: run,
curr_solenoid_values: store,
time_since_launch: launch_time,
show_ui: false
}
}
}
const GRAPH_TIME_MS: u16 = 100;
const MAX_DUTY: u16 = 0xFFF; // 12bit pwm (4096)
const VOLTAGE_HIGH: u16 = 12;
const VOLTAGE_LOW: u16 = 0;
impl crate::window::InterfacePage for IoManipulatorPage {
fn make_ui(&mut self, ui: &mut eframe::egui::Ui, frame: &eframe::Frame) -> crate::window::PageAction {
ui.heading("IO Manipulator");
ui.label("
This UI is only intended for debugging the TCM on a test bench. It is to NEVER be used whilst the TCM is inside a vehicle.
If the TCM detects it is inside a vehicle, it will reject any request.
Upon exiting this page, the TCM will reboot to reset to its default state.
");
if !self.show_ui {
let mut btn_action = None;
ui.horizontal(|row| {
if row.button("I understand").clicked() {
}
if row.button("Take me to safety").clicked() {
btn_action = Some(PageAction::Destroy);
}
});
if let Some(req) = btn_action {
return req;
}
}
PageAction::None
}
fn get_title(&self) -> &'static str {
"IO Manipulator view"
}
fn get_status_bar(&self) -> Option<Box<dyn crate::window::StatusBar>> {
Some(Box::new(self.bar.clone()))
}
}
impl Drop for IoManipulatorPage {
fn drop(&mut self) {
}
}

View File

@ -1,8 +1,8 @@
use std::{sync::{Arc, Mutex, mpsc}, ops::Deref};
use std::{sync::{Arc, Mutex, mpsc}, ops::RangeInclusive};
use ecu_diagnostics::{hardware::{HardwareResult, HardwareScanner, passthru::{PassthruScanner, PassthruDevice}, Hardware, HardwareInfo}, channel::{PayloadChannel, IsoTPChannel}};
use egui::*;
use epi::*;
use ecu_diagnostics::{hardware::{HardwareResult, HardwareScanner, passthru::{PassthruScanner, PassthruDevice}, Hardware}, channel::IsoTPChannel};
use eframe::egui::*;
use eframe::egui;
#[cfg(unix)]
use ecu_diagnostics::hardware::socketcan::{SocketCanScanner, SocketCanDevice};
@ -13,6 +13,8 @@ use crate::{
window::{InterfacePage, PageAction},
};
use super::widgets::range_display::range_display;
type ScanResult = std::result::Result<Vec<String>, String>;
pub struct Launcher {
@ -53,6 +55,7 @@ impl DynamicDevice {
pub fn create_isotp_channel(&mut self) -> HardwareResult<Box<dyn IsoTPChannel>> {
match self {
DynamicDevice::Passthru(pt) => {
PassthruDevice::toggle_sw_channel_raw(pt, true);
Hardware::create_iso_tp_channel(pt.clone())
},
DynamicDevice::Usb(usb) => {
@ -102,7 +105,7 @@ impl Launcher {
}
impl InterfacePage for Launcher {
fn make_ui(&mut self, ui: &mut Ui, frame: &epi::Frame) -> crate::window::PageAction {
fn make_ui(&mut self, ui: &mut Ui, frame: &eframe::Frame) -> crate::window::PageAction {
ui.label("Ultimate-Nag52 configuration utility!");
ui.label("Please plug in your TCM via USB and select the correct port, or select another API");
@ -157,6 +160,9 @@ impl InterfacePage for Launcher {
if let Some(e) = &self.launch_err {
ui.label(RichText::new(format!("Error: {}", e)).color(Color32::from_rgb(255, 0, 0)));
}
range_display(ui, 65.0, 50.0, 70.0, 0.0, 100.0);
crate::window::PageAction::None
}

View File

@ -1,21 +1,17 @@
use std::sync::{Arc, Mutex, mpsc};
use ecu_diagnostics::{
hardware::Hardware,
DiagnosticServer,
kwp2000::{self, Kwp2000DiagnosticServer, Kwp2000ServerOptions, Kwp2000VoidHandler}, channel::IsoTPChannel,
kwp2000::{Kwp2000DiagnosticServer, Kwp2000ServerOptions, Kwp2000VoidHandler}, channel::IsoTPChannel,
};
use egui::*;
use epi::Frame;
use eframe::egui;
use eframe::Frame;
use crate::{
usb_hw::diag_usb::{Nag52USB, EspLogMessage},
window::{InterfacePage, PageAction, StatusBar},
usb_hw::diag_usb::{EspLogMessage},
window::{InterfacePage, PageAction},
};
use super::{firmware_update::FwUpdateUI, status_bar::MainStatusBar, configuration::ConfigPage, crashanalyzer::CrashAnalyzerUI, diagnostics::solenoids::SolenoidPage, };
use ecu_diagnostics::kwp2000::*;
use super::{firmware_update::FwUpdateUI, status_bar::MainStatusBar, configuration::ConfigPage, crashanalyzer::CrashAnalyzerUI, diagnostics::{solenoids::SolenoidPage, shift_reporter::ShiftReportPage}, io_maipulator::IoManipulatorPage, routine_tests::RoutinePage, };
use crate::ui::diagnostics::DiagnosticsPage;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
@ -37,7 +33,7 @@ impl MainPage {
let channel_cfg = ecu_diagnostics::channel::IsoTPSettings {
block_size: 0,
st_min: 0,
extended_addressing: false,
extended_addresses: None,
pad_frame: true,
can_speed: 500_000,
can_use_ext_addr: false,
@ -45,8 +41,8 @@ impl MainPage {
let server_settings = Kwp2000ServerOptions {
send_id: 0x07E1,
recv_id: 0x07E9,
read_timeout_ms: 5000,
write_timeout_ms: 5000,
read_timeout_ms: if logger.is_some() { 1000 } else { 5000 }, // 250ms for USB, 5 seconds for CAN
write_timeout_ms: if logger.is_some() { 1000 } else { 5000 }, // 250ms for USB, 5 seconds for CAN
global_tp_id: 0,
tester_present_interval_ms: 2000,
tester_present_require_response: true,
@ -72,7 +68,7 @@ impl InterfacePage for MainPage {
fn make_ui(
&mut self,
ui: &mut egui::Ui,
frame: &epi::Frame,
frame: &Frame,
) -> crate::window::PageAction {
// UI context menu
egui::menu::bar(ui, |bar_ui| {
@ -111,6 +107,15 @@ impl InterfacePage for MainPage {
if v.button("Solenoid live view").clicked() {
create_page = Some(PageAction::Add(Box::new(SolenoidPage::new(self.diag_server.clone(), self.bar.clone()))));
}
if v.button("Shift report history viewer").clicked() {
create_page = Some(PageAction::Add(Box::new(ShiftReportPage::new(self.diag_server.clone(), self.bar.clone()))));
}
if v.button("IO Manipulator").clicked() {
create_page = Some(PageAction::Add(Box::new(IoManipulatorPage::new(self.diag_server.clone(), self.bar.clone()))));
}
if v.button("Diagnostic routine executor").clicked() {
create_page = Some(PageAction::Add(Box::new(RoutinePage::new(self.diag_server.clone(), self.bar.clone()))));
}
if v.button("Map tuner").clicked() {}
if v.button("Configure drive profiles").clicked() {}
if v.button("Configure vehicle / gearbox").clicked() {

View File

@ -1,4 +1,5 @@
use egui::Color32;
use eframe::egui::Color32;
use eframe::egui;
use crate::window::InterfacePage;
@ -10,6 +11,9 @@ pub mod diagnostics;
pub mod configuration;
pub mod crashanalyzer;
pub mod kwp_event;
pub mod io_maipulator;
pub mod routine_tests;
pub mod widgets;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum StatusText {

View File

@ -0,0 +1,56 @@
use std::sync::{Mutex, Arc};
use ecu_diagnostics::kwp2000::Kwp2000DiagnosticServer;
use crate::window::PageAction;
use self::solenoid_test::SolenoidTestPage;
use super::status_bar::MainStatusBar;
pub mod solenoid_test;
pub struct RoutinePage {
bar: MainStatusBar,
server: Arc<Mutex<Kwp2000DiagnosticServer>>,
}
impl RoutinePage {
pub fn new(server: Arc<Mutex<Kwp2000DiagnosticServer>>, bar: MainStatusBar) -> Self {
Self {
bar,
server
}
}
}
impl crate::window::InterfacePage for RoutinePage {
fn make_ui(&mut self, ui: &mut eframe::egui::Ui, frame: &eframe::Frame) -> crate::window::PageAction {
ui.heading("Diagnostic routines");
ui.label("
Select test routine to run
");
let mut page_action = PageAction::None;
if ui.button("Solenoid test").clicked() {
page_action = PageAction::Add(Box::new(SolenoidTestPage::new(self.server.clone(), self.bar.clone())));
}
page_action
}
fn get_title(&self) -> &'static str {
"Routine executor"
}
fn get_status_bar(&self) -> Option<Box<dyn crate::window::StatusBar>> {
Some(Box::new(self.bar.clone()))
}
}

View File

@ -0,0 +1,220 @@
use std::{sync::{Arc, Mutex, atomic::{AtomicBool, Ordering, AtomicU64, AtomicU8}, RwLock}, thread, time::{Duration, Instant}, char::MAX, collections::btree_map::Range, ops::RangeInclusive};
use ecu_diagnostics::{kwp2000::{Kwp2000DiagnosticServer, SessionType}, DiagServerResult, DiagnosticServer, DiagError};
use eframe::egui::{plot::{Plot, Legend, Line, Values, Value}, widgets, Color32, RichText, self};
use crate::{ui::status_bar::MainStatusBar, window::PageAction};
pub struct SolenoidTestPage {
bar: MainStatusBar,
test_state: Arc<AtomicU8>,
test_result: Arc<RwLock<Option<TestResultsSolenoid>>>,
test_status: Arc<RwLock<String>>,
server: Arc<Mutex<Kwp2000DiagnosticServer>>
}
const TempCoefficient: f32 = 0.393; // Copper coils and wires
const ResistanceMeasureTemp: f32 = 25.0; // Mercedes tests resistance at 25C
// From Sonnax data
const ResitanceMPC: std::ops::RangeInclusive<f32> = (4.0..=8.0); // 6
const ResitanceSPC: std::ops::RangeInclusive<f32> = (4.0..=8.0); // 6
const ResitanceTCC: std::ops::RangeInclusive<f32> = (2.0..=4.0); // 3
const ResitanceY3: std::ops::RangeInclusive<f32> = (2.5..=6.5); // 4.5
const ResitanceY4: std::ops::RangeInclusive<f32> = (2.5..=6.5); // 4.5
const ResitanceY5: std::ops::RangeInclusive<f32> = (2.5..=6.5); // 4.5
#[repr(packed)]
#[derive(Debug, Clone, Copy)]
pub struct TestResultsSolenoid {
atf_temp: i16,
mpc_off_current: u16,
spc_off_current: u16,
tcc_off_current: u16,
y3_off_current: u16,
y4_off_current: u16,
y5_off_current: u16,
vbatt_mpc: u16,
mpc_on_current: u16,
vbatt_spc: u16,
spc_on_current: u16,
vbatt_tcc: u16,
tcc_on_current: u16,
vbatt_y3: u16,
y3_on_current: u16,
vbatt_y4: u16,
y4_on_current: u16,
vbatt_y5: u16,
y5_on_current: u16,
}
impl SolenoidTestPage {
pub fn new(server: Arc<Mutex<Kwp2000DiagnosticServer>>, bar: MainStatusBar) -> Self {
Self {
bar,
server,
test_state: Arc::new(AtomicU8::new(0)),
test_result: Arc::new(RwLock::new(None)),
test_status: Arc::new(RwLock::new(String::new()))
}
}
}
fn calc_resistance(current: u16, batt: u16, temp: i16) -> f32 {
let resistance_now = batt as f32 / current as f32;
return resistance_now + resistance_now*(((ResistanceMeasureTemp-temp as f32)*TempCoefficient)/100.0)
}
fn make_resistance_text(c_raw: u16, r: f32, range: RangeInclusive<f32>) -> egui::Label {
if c_raw == 0 {
return egui::Label::new(RichText::new("FAIL! Open circuit detected!").color(Color32::RED))
}
if (c_raw > 3200 && range != ResitanceTCC) {
return egui::Label::new(RichText::new("FAIL! Short circuit detected!").color(Color32::RED))
}
let c: Color32;
let t: String;
if range.contains(&r) {
c = Color32::GREEN;
t = format!("OK! ({:.2}Ω). Acceptable range is {:.2}..{:.2}Ω", r, range.start(), range.end());
} else {
c = Color32::RED;
t = format!("FAIL! ({:.2}Ω). Acceptable range is {:.2}..{:.2}Ω", r, range.start(), range.end());
}
egui::Label::new(RichText::new(t).color(c))
}
impl crate::window::InterfacePage for SolenoidTestPage {
fn make_ui(&mut self, ui: &mut eframe::egui::Ui, frame: &eframe::Frame) -> crate::window::PageAction {
ui.heading("Solenoid test");
ui.label("
This test will test each solenoid within the transmission, and ensure
that their resistance is within specification, and that there is no short circuit within the valve
body.
");
ui.separator();
ui.label("
TEST REQUIRMENTS:
1. Shifter in D or R
2. Engine off
3. Not moving
4. Battery at least 11.5V
");
let state = self.test_state.load(Ordering::Relaxed);
if state == 1 { // Running
ui.label("Test running...");
} else {
if ui.button("Begin test").clicked() {
let ctx = ui.ctx().clone();
let s = self.server.clone();
let str_ref = self.test_status.clone();
let state_ref = self.test_state.clone();
let res_ref = self.test_result.clone();
std::thread::spawn(move|| {
state_ref.store(1, Ordering::Relaxed);
let mut guard = s.lock().unwrap();
if let Err(e) = guard.set_diagnostic_session_mode(SessionType::ExtendedDiagnostics) {
*str_ref.write().unwrap() = format!("ECU failed to enter extended diagnostic mode: {}", e);
state_ref.store(2, Ordering::Relaxed);
ctx.request_repaint();
return;
}
if let Err(e) = guard.send_byte_array_with_response(&[0x31, 0xDE]) {
let _ = guard.set_diagnostic_session_mode(SessionType::Normal);
*str_ref.write().unwrap() = format!("ECU rejected the test: {}", e);
state_ref.store(2, Ordering::Relaxed);
ctx.request_repaint();
return;
}
loop {
match guard.send_byte_array_with_response(&[0x33, 0xDE]) { // Request test results in a loop
Ok(res) => {
let routine_res_ptr: *const TestResultsSolenoid = res[2..].as_ptr() as * const TestResultsSolenoid;
let routine_res: TestResultsSolenoid = unsafe { *routine_res_ptr };
*res_ref.write().unwrap() = Some(routine_res);
*str_ref.write().unwrap() = format!("ECU Test Completed!");
break;
},
Err(e) => {
let mut failed = true;
if let DiagError::ECUError { code, def: _ } = e {
if code == 0x22 { failed = false; } // Just waiting for test to finish!
}
if failed {
*str_ref.write().unwrap() = format!("Failed to get ECU test results: {}", e);
break;
}
}
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
let _ = guard.set_diagnostic_session_mode(SessionType::Normal);
state_ref.store(2, Ordering::Relaxed);
ctx.request_repaint();
});
}
if state == 2 {
ui.label(self.test_status.read().unwrap().as_str());
if let Some(results) = self.test_result.read().unwrap().clone() {
ui.separator();
ui.label(format!("ATF Temp was {} C. Showing results adjusted to {} C", &{results.atf_temp}, ResistanceMeasureTemp));
egui::Grid::new("S").striped(true).show(ui, |g_ui|{
g_ui.label("MPC Solenoid");
g_ui.add(make_resistance_text(results.mpc_on_current, calc_resistance(results.mpc_on_current, results.vbatt_mpc, results.atf_temp), ResitanceMPC));
g_ui.end_row();
g_ui.label("SPC Solenoid");
g_ui.add(make_resistance_text(results.spc_on_current, calc_resistance(results.spc_on_current, results.vbatt_spc, results.atf_temp), ResitanceSPC));
g_ui.end_row();
g_ui.label("TCC Solenoid");
g_ui.add(make_resistance_text(results.tcc_on_current, calc_resistance(results.tcc_on_current, results.vbatt_tcc, results.atf_temp), ResitanceTCC));
g_ui.end_row();
g_ui.label("Y3 shift Solenoid");
g_ui.add(make_resistance_text(results.y3_on_current, calc_resistance(results.y3_on_current, results.vbatt_y3, results.atf_temp), ResitanceY3));
g_ui.end_row();
g_ui.label("Y4 shift Solenoid");
g_ui.add(make_resistance_text(results.y4_on_current, calc_resistance(results.y4_on_current, results.vbatt_y4, results.atf_temp), ResitanceY4));
g_ui.end_row();
g_ui.label("Y5 shift Solenoid");
g_ui.add(make_resistance_text(results.y5_on_current, calc_resistance(results.y5_on_current, results.vbatt_y5, results.atf_temp), ResitanceY5));
g_ui.end_row();
});
}
}
}
PageAction::None
}
fn get_title(&self) -> &'static str {
"IO Manipulator view"
}
fn get_status_bar(&self) -> Option<Box<dyn crate::window::StatusBar>> {
Some(Box::new(self.bar.clone()))
}
}

View File

@ -1,6 +1,5 @@
use ecu_diagnostics::hardware::Hardware;
use egui::*;
use epi::*;
use eframe::egui::*;
use std::{
collections::VecDeque,
sync::{Arc, Mutex, RwLock, mpsc}, borrow::BorrowMut,
@ -10,6 +9,7 @@ use crate::{
usb_hw::diag_usb::{EspLogMessage, Nag52USB},
window::{InterfacePage, StatusBar},
};
use eframe::egui;
#[derive(Clone)]
pub struct MainStatusBar {

View File

@ -0,0 +1 @@
pub mod range_display;

View File

@ -0,0 +1,51 @@
use std::{ops::RangeInclusive, fmt::Display};
use eframe::{egui, epaint::{Rounding, Color32, FontId}, emath::{Rect, Pos2, Align2}};
pub fn range_display<T: Into<f32> + Copy + Display>(ui: &mut egui::Ui,
curr_value: T,
min_ok: T,
max_ok: T,
min_display: T,
max_display: T
) -> egui::Response {
let desired_size = egui::Vec2::new(ui.available_width(), 40.0);
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
let bar_region = Rect::from_two_pos(
Pos2::new(rect.left(), rect.bottom()-10.0),
Pos2::new(rect.right(), rect.top())
);
let total_size_per_range = desired_size.x / (max_display.into()-min_display.into());
let ok_region_rect = Rect::from_two_pos(
Pos2::new(bar_region.left()+(total_size_per_range*(min_ok.into()-min_display.into())), bar_region.bottom()),
Pos2::new(bar_region.left()+(total_size_per_range*(min_ok.into()-min_display.into()))+total_size_per_range*(max_ok.into()-min_ok.into()), rect.top())
);
let value_region = Rect::from_two_pos(
Pos2::new(bar_region.left()+(total_size_per_range*(curr_value.into()-min_display.into())), bar_region.bottom()),
Pos2::new(bar_region.left()+(total_size_per_range*(curr_value.into()-min_display.into()))+2.0, rect.top())
);
// OK range
let visuals = ui.style().noninteractive().clone();
let painter = ui.painter();
painter.rect(bar_region, Rounding::from(4.0), Color32::RED, visuals.bg_stroke);
painter.rect(ok_region_rect, Rounding::none(), Color32::GREEN, visuals.fg_stroke);
painter.rect(value_region, Rounding::none(), Color32::BLACK, visuals.fg_stroke);
painter.text(Pos2::new(rect.left(), rect.bottom()+5.0), Align2::LEFT_BOTTOM, format!("{:.2}", min_display), FontId::default(), visuals.text_color());
painter.text(Pos2::new(rect.right(), rect.bottom()+5.0), Align2::RIGHT_BOTTOM, format!("{:.2}", max_display), FontId::default(), visuals.text_color());
painter.text(Pos2::new(ok_region_rect.left(), rect.bottom()+5.0), Align2::CENTER_BOTTOM, format!("{:.2}", min_ok), FontId::default(), visuals.text_color());
painter.text(Pos2::new(ok_region_rect.right(), rect.bottom()+5.0), Align2::CENTER_BOTTOM, format!("{:.2}", max_ok), FontId::default(), visuals.text_color());
painter.text(Pos2::new(value_region.right(), rect.bottom()+5.0), Align2::CENTER_BOTTOM, format!("{:.2}", curr_value), FontId::default(), visuals.text_color());
response
}

View File

@ -9,14 +9,13 @@ use std::{
mpsc::{self},
Arc,
},
time::{Instant, Duration},
time::{Instant, Duration}, panic::catch_unwind,
};
use std::fmt::Write as SWrite;
use ecu_diagnostics::{
channel::{IsoTPChannel, PayloadChannel, ChannelError},
hardware::{HardwareInfo, HardwareResult, HardwareError},
};
use egui::mutex::Mutex;
use serial_rs::{SerialPort, PortInfo, SerialPortSettings, FlowControl};
#[derive(Debug, Clone, Copy)]
@ -66,6 +65,8 @@ impl Nag52USB {
let is_running = Arc::new(AtomicBool::new(true));
let is_running_r = is_running.clone();
port.clear_input_buffer();
port.clear_output_buffer();
let mut port_clone = port.try_clone().unwrap();
// Create 2 threads, one to read the port, one to write to it
@ -75,57 +76,36 @@ impl Nag52USB {
let mut reader = BufReader::new(&mut port_clone);
let mut line = String::new();
loop {
line.clear();
if reader.read_line(&mut line).is_ok() {
let _ = line.pop();
if line.chars().next().unwrap() == '#' {
line.pop();
println!("LINE: {}", line);
if line.is_empty() {continue;}
if line.starts_with("#") || line.starts_with("07E9") {
// First char is #, diag message
// Diag message
if line.len() % 2 == 0 {
if line.starts_with("#") {
line.remove(0);
}
if line.len() % 2 != 0 {
eprintln!("Discarding invalid diag msg '{}'", line);
} else {
//println!("Read diag payload {:?} from Nag52 USB", line);
let contents: &str = &line[1..];
let can_id = u32::from_str_radix(&contents[0..4], 16).unwrap();
let payload: Vec<u8> = (4..contents.len())
.step_by(2)
.map(|i| u8::from_str_radix(&contents[i..i + 2], 16).unwrap())
.collect();
read_tx_diag.send((can_id, payload));
let can_id = u32::from_str_radix(&line[0..4], 16).unwrap();
if let Ok(p) = catch_unwind(||{
let payload: Vec<u8> = (4..line.len())
.step_by(2)
.map(|i| u8::from_str_radix(&line[i..i + 2], 16).unwrap())
.collect();
payload
}) {
read_tx_diag.send((can_id, p));
}
}
} else {
// This is a log message
if line.len() < 5 {
continue;
}
if !line.contains("(") || !line.contains(")") {
println!("EEE {}", line);
continue;
}
let lvl = match line.chars().next().unwrap() {
'I' => EspLogLevel::Info,
'W' => EspLogLevel::Warn,
'E' => EspLogLevel::Error,
'D' => EspLogLevel::Debug,
_ => continue,
};
let timestamp = u128::from_str_radix(
line.split_once(")").unwrap().0.split_once("(").unwrap().1,
10,
)
.unwrap();
let split = line.split_once(": ").unwrap();
let msg = EspLogMessage {
lvl,
timestamp,
tag: split.0.split_once(")").unwrap().1.to_string(),
msg: split.1.to_string(),
};
println!("{:?}", msg);
read_tx_log.send(msg);
//read_tx_log.send(msg);
}
line.clear();
}
//std::thread::sleep(from_millis(10));
}
}
println!("Serial reader stop");
@ -233,7 +213,7 @@ impl PayloadChannel for Nag52USB {
fn read_bytes(&mut self, timeout_ms: u32) -> ecu_diagnostics::channel::ChannelResult<Vec<u8>> {
let now = Instant::now();
while now.elapsed().as_millis() < timeout_ms as u128 {
if let Ok((id, data)) = self.rx_diag.recv() {
if let Ok((id, data)) = self.rx_diag.try_recv() {
if id == self.rx_id {
return Ok(data);
}

View File

@ -1,10 +1,7 @@
use std::{borrow::BorrowMut, collections::VecDeque};
use ecu_diagnostics::hardware::Hardware;
use egui::*;
use epi::*;
use crate::ui::status_bar::{self};
use eframe::egui;
pub struct MainWindow {
pages: VecDeque<Box<dyn InterfacePage>>,
@ -37,14 +34,13 @@ impl MainWindow {
}
}
impl epi::App for MainWindow {
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
impl eframe::App for MainWindow {
fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) {
let stack_size = self.pages.len();
if stack_size > 0 {
let mut pop_page = false;
if let Some(status_bar) = self.bar.borrow_mut() {
egui::TopBottomPanel::bottom("NAV")
.default_height(800.0)
.show(ctx, |nav| {
nav.horizontal(|row| {
status_bar.draw(row, ctx);
@ -78,16 +74,7 @@ impl epi::App for MainWindow {
}
//ctx.request_repaint(); // Continuous mode
}
fn name(&self) -> &str {
if self.pages.len() > 0 {
self.pages[0].get_title()
} else {
"Ultimate-Nag52 configuration utility"
}
}
}
pub enum PageAction {
None,
Destroy,
@ -98,7 +85,7 @@ pub enum PageAction {
}
pub trait InterfacePage {
fn make_ui(&mut self, ui: &mut egui::Ui, frame: &epi::Frame) -> PageAction;
fn make_ui(&mut self, ui: &mut egui::Ui, frame: &eframe::Frame) -> PageAction;
fn get_title(&self) -> &'static str;
fn get_status_bar(&self) -> Option<Box<dyn StatusBar>>;
}