Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e93d155
sort of deciding on a test format
LelsersLasers Jun 11, 2026
4b9be05
schema for tests
LelsersLasers Jun 11, 2026
9160543
serde to read configs
LelsersLasers Jun 11, 2026
56be294
able to list all presets and tests (untested)
LelsersLasers Jun 13, 2026
cfe0ffc
Fix config loading
LelsersLasers Jun 13, 2026
88fbf06
Wiring HIL widget + trying to think about UI state
LelsersLasers Jun 13, 2026
e280601
System to kind of load a test
LelsersLasers Jun 13, 2026
0a1eb3c
start from preset
LelsersLasers Jun 13, 2026
74d7c19
Basic UI started for in progress tests
LelsersLasers Jun 13, 2026
2a5513d
Trying to work on UI
LelsersLasers Jun 13, 2026
2be4529
improve in progress UI
LelsersLasers Jun 13, 2026
5b92ed4
Test summary UI
LelsersLasers Jun 13, 2026
09e4847
Breaking into helper functions
LelsersLasers Jun 13, 2026
50cab5c
Thinking about HIL state machine
LelsersLasers Jun 13, 2026
01a17f3
update heartbeat tests to be accurate
LelsersLasers Jun 13, 2026
c94c475
sorting tests by basename, presets by name
LelsersLasers Jun 13, 2026
cae4fd1
Actually running expects + can stop a test midway
LelsersLasers Jun 13, 2026
5bbe88c
fmt :skull:
LelsersLasers Jun 13, 2026
ee8ff03
remove unused import
LelsersLasers Jun 13, 2026
6999b24
Clear start errors on new start attempt
LelsersLasers Jun 13, 2026
026b17a
Clear start error on reload
LelsersLasers Jun 13, 2026
e8eecef
actually displaying start error
LelsersLasers Jun 13, 2026
9872eb5
total_cmp so NaN's don't nuke?
LelsersLasers Jun 13, 2026
2ea6307
Clean loading + filter for .json
LelsersLasers Jun 13, 2026
5bc3c22
Fix typos
LelsersLasers Jun 13, 2026
947e46a
typos
LelsersLasers Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions hil_config/presets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"All Heartbeats": [ "heartbeats_crit", "heartbeats_non_crit" ]
}
86 changes: 86 additions & 0 deletions hil_config/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",

"required": ["name", "description", "expect"],

"properties": {
"$schema": {
"type": "string"
},

"name": {
"type": "string"
},

"description": {
"type": "string"
},

"tx": {
"type": "array",
"items": {
"type": "object",
"required": ["timestamp", "msg_name", "signals"],

"properties": {
"timestamp": {
"type": "number"
},

"msg_name": {
"type": "string"
},

"signals": {
"type": "object",
"additionalProperties": {
"type": "number"
}
}
},

"additionalProperties": false
}
},

"expect": {
"type": "array",
"items": {
"type": "object",
"required": ["window", "msg_name"],

"properties": {
"window": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 2,
"maxItems": 2
},

"msg_name": {
"type": "string"
},

"signals": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 2,
"maxItems": 2
}
}
},

"additionalProperties": false
}
}
},

"additionalProperties": false
}
25 changes: 25 additions & 0 deletions hil_config/tests/heartbeats_crit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "../schema.json",

"name": "Critical Heartbeats",
"description": "Check that we receive a heartbeat (version hash) from every drive critical board",

"expect": [
{
"window": [0, 10000],
"msg_name": "abox_version"
},
{
"window": [0, 10000],
"msg_name": "dash_version"
},
{
"window": [0, 10000],
"msg_name": "main_version"
},
{
"window": [0, 10000],
"msg_name": "pdu_version"
}
]
}
17 changes: 17 additions & 0 deletions hil_config/tests/heartbeats_non_crit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "../schema.json",

"name": "Non-Critical Heartbeats",
"description": "Check that we receive a heartbeat (version hash) from non-critical boards",

"expect": [
{
"window": [0, 10000],
"msg_name": "front_driveline_version"
},
{
"window": [0, 10000],
"msg_name": "rear_driveline_version"
}
]
}
27 changes: 27 additions & 0 deletions hil_config/tests/idk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "../schema.json",

"name": "Pack sanity check",
"description": "Verify thermistor response",

"tx": [
{
"timestamp": 15,
"msg_name": "pack_stats",
"signals": {
"pack_voltage": 540.0,
"pack_current": 10.0,
"max_temp": 35.0
}
}
],
"expect": [
{
"window": [15, 20],
"msg_name": "thermistor_telemetry",
"signals": {
"temperature": [30.0, 40.0]
}
}
]
}
2 changes: 2 additions & 0 deletions src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub enum WidgetType {
GgPlot,
Dynamics,
Jitter,
Hil,
}

impl AppAction {
Expand All @@ -41,6 +42,7 @@ impl AppAction {
("Spawn G-G Plot", WidgetType::GgPlot),
("Spawn Dynamics", WidgetType::Dynamics),
("Spawn Jitter", WidgetType::Jitter),
("Spawn HIL", WidgetType::Hil),
]
}
}
8 changes: 8 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub struct DAQApp {
pub next_gg_plot_num: usize,
pub next_dynamics_num: usize,
pub next_jitter_num: usize,
pub next_hil_num: usize,
pub can_to_ui_rx: std::sync::mpsc::Receiver<messages::MsgFromCan>,
pub ui_to_can_tx: std::sync::mpsc::Sender<messages::MsgFromUi>,
pub action_queue: Vec<action::AppAction>,
Expand Down Expand Up @@ -107,6 +108,7 @@ impl DAQApp {
next_gg_plot_num: 1,
next_dynamics_num: 1,
next_jitter_num: 1,
next_hil_num: 1,
can_to_ui_rx,
ui_to_can_tx,
action_queue: Vec::new(),
Expand Down Expand Up @@ -211,6 +213,9 @@ impl DAQApp {
action::WidgetType::Jitter => {
widgets::Widget::Jitter(ui::jitter::Jitter::new(self.next_jitter_num))
}
action::WidgetType::Hil => {
widgets::Widget::Hil(ui::hil::Hil::new(self.next_hil_num))
}
};
self.add_widget_to_tree(widget);

Expand Down Expand Up @@ -252,6 +257,9 @@ impl DAQApp {
action::WidgetType::Jitter => {
self.next_jitter_num += 1;
}
action::WidgetType::Hil => {
self.next_hil_num += 1;
}
}
}
action::AppAction::ToggleSidebar => {
Expand Down
159 changes: 159 additions & 0 deletions src/hil/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Read HIL config files (preset and tests)

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

pub const PRESETS_FILE: &str = "hil_config/presets.json";
pub const TESTS_FOLDER: &str = "hil_config/tests/";
Comment thread
LelsersLasers marked this conversation as resolved.

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PresetsFile(pub HashMap<String, Vec<String>>);

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestFile {
pub name: String,
pub description: String,

#[serde(default)]
pub tx: Vec<TxMessage>,

pub expect: Vec<Expectation>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxMessage {
pub timestamp: f64,
pub msg_name: String,

pub signals: HashMap<String, f64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Expectation {
pub window: [f64; 2],

pub msg_name: String,

#[serde(default)]
pub signals: HashMap<String, [f64; 2]>,
}

#[derive(Clone)]
pub struct PresetInfo {
pub name: String,
// base names
pub tests: Vec<String>,
}

#[derive(Clone)]
pub struct TestInfo {
pub basename: String,
pub name: String,
pub description: String,
}

pub fn list_available_tests() -> (Vec<PresetInfo>, Vec<TestInfo>, Vec<String>) {
let mut errors = Vec::new();

// Load individual tests
let mut individual_tests = Vec::new();
if let Ok(entries) = std::fs::read_dir(TESTS_FOLDER) {
for entry in entries.flatten() {
let path = entry.path();
if !path.is_file() || path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}

let basename = match path.file_stem().and_then(|s| s.to_str()) {
Some(stem) if !stem.is_empty() => stem.to_string(),
_ => {
errors.push(format!(
"Failed to determine test basename for file: {:?}",
path
));
continue;
}
};

let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => {
errors.push(format!("Failed to read test file: {:?}", path));
continue;
}
};

match serde_json::from_str::<TestFile>(&content) {
Ok(test_data) => individual_tests.push(TestInfo {
basename,
name: test_data.name,
description: test_data.description,
}),
Err(_) => errors.push(format!("Failed to parse test file: {:?}", path)),
};
}
} else {
errors.push(format!("Failed to read tests directory: {}", TESTS_FOLDER));
}
individual_tests.sort_by(|a, b| a.basename.to_lowercase().cmp(&b.basename.to_lowercase()));

// Load presets
let mut presets = Vec::new();
if let Ok(presets_file) = std::fs::read_to_string(PRESETS_FILE) {
if let Ok(presets_data) = serde_json::from_str::<PresetsFile>(&presets_file) {
for (preset_name, subtests) in presets_data.0 {
if subtests.is_empty() {
errors.push(format!("Preset '{}' has no subtests defined", preset_name));
continue;
}

if subtests.iter().any(|s| s.trim().is_empty()) {
errors.push(format!(
"Preset '{}' contains empty subtest names",
preset_name
));
continue;
}

if subtests
.iter()
.any(|s| individual_tests.iter().all(|t| t.basename != *s))
{
errors.push(format!(
"Preset '{}' contains subtests that do not exist: {:?}",
preset_name,
subtests
.iter()
.filter(|s| individual_tests.iter().all(|t| t.basename != **s))
.collect::<Vec<_>>()
));
continue;
}

presets.push(PresetInfo {
name: preset_name,
tests: subtests,
});
}
} else {
errors.push(format!("Failed to parse presets file: {}", PRESETS_FILE));
}
} else {
errors.push(format!("Failed to read presets file: {}", PRESETS_FILE));
}
presets.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));

(presets, individual_tests, errors)
}

pub fn load_test_from_file(basename: &str) -> Result<TestFile, String> {
let path = format!("{}{}.json", TESTS_FOLDER, basename);
match std::fs::read_to_string(&path) {
Ok(content) => match serde_json::from_str::<TestFile>(&content) {
Ok(test_data) => Ok(test_data),
Err(_) => Err(format!("Failed to parse test file: {}", path)),
},
Err(_) => Err(format!("Failed to read test file: {}", path)),
}
}
2 changes: 2 additions & 0 deletions src/hil/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod config;
pub mod run;
Loading
Loading