diff --git a/.gitmodules b/.gitmodules index f18e99b..8193e62 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "Embedded-Base"] path = Embedded-Base - url = ../Embedded-Base + url = https://github.com/Northeastern-Electric-Racing/Embedded-Base.git + branch = main diff --git a/Cargo.lock b/Cargo.lock index 1a6d6e0..ef9c2bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,6 +136,8 @@ dependencies = [ "proc-macro2", "quote", "serde", + "serde_json", + "thiserror 2.0.6", ] [[package]] @@ -184,7 +186,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -345,7 +347,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -561,7 +563,7 @@ dependencies = [ "libc", "log", "paho-mqtt-sys", - "thiserror", + "thiserror 1.0.50", ] [[package]] @@ -604,7 +606,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -660,7 +662,7 @@ checksum = "0bcc343da15609eaecd65f8aa76df8dc4209d325131d8219358c0aaaebab0bf6" dependencies = [ "once_cell", "protobuf-support", - "thiserror", + "thiserror 1.0.50", ] [[package]] @@ -675,7 +677,7 @@ dependencies = [ "protobuf-parse", "regex", "tempfile", - "thiserror", + "thiserror 1.0.50", ] [[package]] @@ -690,7 +692,7 @@ dependencies = [ "protobuf", "protobuf-support", "tempfile", - "thiserror", + "thiserror 1.0.50", "which", ] @@ -700,7 +702,7 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0766e3675a627c327e4b3964582594b0e8741305d628a98a5de75a1d15f99b9" dependencies = [ - "thiserror", + "thiserror 1.0.50", ] [[package]] @@ -816,7 +818,7 @@ checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -862,7 +864,7 @@ dependencies = [ "nb", "neli", "nix", - "thiserror", + "thiserror 1.0.50", ] [[package]] @@ -884,9 +886,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.85" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -912,7 +914,16 @@ version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.50", +] + +[[package]] +name = "thiserror" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +dependencies = [ + "thiserror-impl 2.0.6", ] [[package]] @@ -923,7 +934,18 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", ] [[package]] @@ -1046,5 +1068,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] diff --git a/Cargo.toml b/Cargo.toml index 6e27d64..f0da11b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ daedalus = { path = "./libs/daedalus" } [build-dependencies] protobuf-codegen = "3.5.1" +calypso-cangen = { path = "./libs/calypso-cangen" } [profile.release] lto = true diff --git a/Embedded-Base b/Embedded-Base index 5d4b634..c2061ae 160000 --- a/Embedded-Base +++ b/Embedded-Base @@ -1 +1 @@ -Subproject commit 5d4b634132fa9dc44e4f9496c265c6e214715075 +Subproject commit c2061ae62cf670cdb6fb6ac9c966f3ec74ffc916 diff --git a/build.rs b/build.rs index eebb270..7ce53c1 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,6 @@ +use calypso_cangen::validate::*; +use std::process; + /* Prebuild script */ fn main() { println!("cargo:rerun-if-changed=Embedded-Base"); @@ -12,4 +15,16 @@ fn main() { // Specify output directory relative to Cargo output directory. .out_dir("src") .run_from_script(); + + // Validate CAN spec + match validate_all_spec() { + Ok(()) => {} + Err(errors) => { + for error in errors { + // The \x1b[...m is an ANSI escape sequence for colored terminal output + println!("\x1b[31;1mCAN spec error:\x1b[0m {}", error); + } + process::exit(1); + } + } } diff --git a/libs/calypso-cangen/Cargo.toml b/libs/calypso-cangen/Cargo.toml index 51165b5..27d1f1e 100644 --- a/libs/calypso-cangen/Cargo.toml +++ b/libs/calypso-cangen/Cargo.toml @@ -7,3 +7,5 @@ edition = "2021" proc-macro2.workspace = true quote.workspace = true serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +thiserror = "2.0.6" diff --git a/libs/calypso-cangen/src/can_gen_encode.rs b/libs/calypso-cangen/src/can_gen_encode.rs index 03ec9f3..2caaf66 100644 --- a/libs/calypso-cangen/src/can_gen_encode.rs +++ b/libs/calypso-cangen/src/can_gen_encode.rs @@ -101,8 +101,8 @@ impl CANGenEncode for CANPoint { } _ => quote! {}, }; - let default_value: f32 = match self.default_value { - Some(default_value) => default_value, + let default_value: f32 = match self.default { + Some(default) => default, _ => 0f32, }; let float_final = quote! { diff --git a/libs/calypso-cangen/src/can_types.rs b/libs/calypso-cangen/src/can_types.rs index 242e3aa..bfdb6a2 100644 --- a/libs/calypso-cangen/src/can_types.rs +++ b/libs/calypso-cangen/src/can_types.rs @@ -10,6 +10,7 @@ use serde::Deserialize; * Class representing a CAN message */ #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct CANMsg { pub id: String, pub desc: String, @@ -23,6 +24,7 @@ pub struct CANMsg { * Class representing a NetField of a CAN message */ #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct NetField { pub name: String, pub unit: String, @@ -36,17 +38,18 @@ pub struct NetField { * Class representing a CAN point of a NetField */ #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct CANPoint { pub size: usize, pub signed: Option, pub endianness: Option, pub format: Option, - pub default_value: Option, + pub default: Option, pub ieee754_f32: Option, } #[derive(Deserialize, Debug)] -#[serde(untagged)] +#[serde(untagged, deny_unknown_fields)] pub enum Sim { SimSweep { min: f32, diff --git a/libs/calypso-cangen/src/lib.rs b/libs/calypso-cangen/src/lib.rs index e294d79..7cbf95b 100644 --- a/libs/calypso-cangen/src/lib.rs +++ b/libs/calypso-cangen/src/lib.rs @@ -1,3 +1,10 @@ pub mod can_gen_decode; pub mod can_gen_encode; pub mod can_types; +pub mod validate; +/** + * Path to CAN spec JSON files + * Used by all daedalus macros + * Filepath is relative to project root (i.e. /Calypso) + */ +pub const CANGEN_SPEC_PATH: &str = "./Embedded-Base/cangen/can-messages"; diff --git a/libs/calypso-cangen/src/validate.rs b/libs/calypso-cangen/src/validate.rs new file mode 100644 index 0000000..b0fcee2 --- /dev/null +++ b/libs/calypso-cangen/src/validate.rs @@ -0,0 +1,220 @@ +use crate::can_types::*; +use crate::CANGEN_SPEC_PATH; +use std::fs; +use std::io::Read; +use std::path::PathBuf; +use thiserror::Error; + +/** + * JSON spec error enum + */ +#[derive(Error, Debug)] +pub enum CANSpecError { + #[error("Message {0} description ({1}) contains illegal characters. Message descriptions may only contain letters and whitespace (_ included).")] + MessageDescIllegalChars(String, String), + + #[error("Message {0} ({1}) totals to {2} bits. Message totals should be byte-aligned (bit size should be a power of 2).")] + MessageTotalByteMisalignment(String, String, usize), + + #[error("Sim frequencies for NetField {0} add to {1}. Sim enum frequencies must add up to 1.")] + FieldSimEnumFrequencySum(String, f32), + + #[error("Point {0} of NetField {1} is {2} bits. The maximum size for a point is 32 bits.")] + PointSizeOverMax(usize, String, usize), + + #[error( + "Signed point {0} of NetField {1} is {2} bits. Signed points must be 8, 16, or 32 bits." + )] + PointSignedBitCount(usize, String, usize), + + #[error("Little-endian point {0} of NetField {1} is {2} bits. Little-endian points must be 8, 16, or 32 bits.")] + PointLittleEndianBitCount(usize, String, usize), + + #[error("Point {0} of NetField {1} specifies endianness and is {2} bits. Points with <=8 bits should not specify endianness.")] + PointSmallSizeEndianness(usize, String, usize), + + #[error("IEEE754 float point {0} of NetField {1} is {2} bits, instead of 32 bits.")] + PointFloatBitCount(usize, String, usize), + + #[error(transparent)] // Pass-through for IO error + IOError(#[from] std::io::Error), +} + +/** + * Validate all CAN spec files in CANGEN_SPEC_PATH + */ +pub fn validate_all_spec() -> Result<(), Vec> { + let mut __all_errors = Vec::new(); + match fs::read_dir(CANGEN_SPEC_PATH) { + Ok(__entries) => { + for __entry in __entries { + match __entry { + Ok(__entry) => { + let __path = __entry.path(); + if __path.is_file() && __path.extension().map_or(false, |ext| ext == "json") + { + match validate_spec_file(__path.clone()) { + Ok(()) => {} + Err(__file_errors) => __all_errors.extend(__file_errors), + }; + } + } + Err(__err) => __all_errors.push(__err.into()), + } + } + + if __all_errors.is_empty() { + Ok(()) + } else { + Err(__all_errors) + } + } + Err(__err) => { + __all_errors.push(__err.into()); + Err(__all_errors) + } + } +} + +/** + * Validate a CAN spec file + */ +fn validate_spec_file(_path: PathBuf) -> Result<(), Vec> { + let mut _errors = Vec::new(); + match fs::File::open(_path) { + Ok(mut _file) => { + let mut _contents = String::new(); + let _ = _file.read_to_string(&mut _contents); + let _msgs: Vec = serde_json::from_str(&_contents).unwrap(); + for _msg in _msgs { + match validate_msg(_msg) { + Ok(()) => {} + Err(_msg_errors) => _errors.extend(_msg_errors), + }; + } + + if _errors.is_empty() { + Ok(()) + } else { + Err(_errors) + } + } + Err(_err) => { + _errors.push(_err.into()); + Err(_errors) + } + } +} + +/** + * Validate a CANMsg + */ +fn validate_msg(_msg: CANMsg) -> Result<(), Vec> { + let mut _errors = Vec::new(); + + // Sum bit count of points for checks + let mut _bit_count: usize = 0; + + // Check description contains legal chars + let _desc = _msg.desc.clone(); + if !_desc + .chars() + .all(|c| c.is_alphabetic() || c.is_whitespace() || c == '_') + { + _errors.push(CANSpecError::MessageDescIllegalChars( + _msg.id.clone(), + _desc, + )); + } + + for _field in _msg.fields { + let _name = _field.name.clone(); + + // Check Sim enum frequencies add to 1 (roughly, f32s are approximate) + if let Some(Sim::SimEnum { options }) = _field.sim { + let mut _sim_total: f32 = 0.0; + options.iter().for_each(|opt| { _sim_total += opt[1]; }); + if (_sim_total - 1.0).abs() > 0.00001 { + _errors.push(CANSpecError::FieldSimEnumFrequencySum( + _name.clone(), + _sim_total, + )); + } + } + + let _send = match _field.send { + Some(false) => false, + _ => true + }; + + for (_i, _point) in _field.points.iter().enumerate() { + _bit_count += _point.size; + + // Check that point size is at most 32 bits + if _point.size > 32 && _send { + _errors.push(CANSpecError::PointSizeOverMax( + _i, + _name.clone(), + _point.size, + )); + continue; + } + + // Check signed point bit count + if let Some(true) = _point.signed { + if _point.size != 8 && _point.size != 16 && _point.size != 32 { + _errors.push(CANSpecError::PointSignedBitCount( + _i, + _name.clone(), + _point.size, + )); + } + } + + if let Some(ref s) = _point.endianness { + // Check that small points don't specify endianness + if _point.size <= 8 { + _errors.push(CANSpecError::PointSmallSizeEndianness( + _i, + _name.clone(), + _point.size, + )); + } + // Check little endian point bit count + else if s == "little" && _point.size != 8 && _point.size != 16 && _point.size != 32 { + _errors.push(CANSpecError::PointLittleEndianBitCount( + _i, + _name.clone(), + _point.size, + )); + } + } + + // Check IEEE754 f32 point bit count + if let Some(true) = _point.ieee754_f32 { + if _point.size != 32 { + _errors.push(CANSpecError::PointFloatBitCount( + _i, + _name.clone(), + _point.size, + )); + } + } + } + } + + // Check message total alignment + if _bit_count % 8 != 0 { + _errors.push(CANSpecError::MessageTotalByteMisalignment( + _msg.id.clone(), + _msg.desc.clone(), + _bit_count, + )); + } + + if _errors.is_empty() { + Ok(()) + } else { + Err(_errors) + } +} diff --git a/libs/daedalus/src/lib.rs b/libs/daedalus/src/lib.rs index 7077889..844b892 100644 --- a/libs/daedalus/src/lib.rs +++ b/libs/daedalus/src/lib.rs @@ -5,6 +5,7 @@ extern crate serde_json; use calypso_cangen::can_gen_decode::*; use calypso_cangen::can_gen_encode::*; use calypso_cangen::can_types::*; +use calypso_cangen::CANGEN_SPEC_PATH; use proc_macro::TokenStream; use proc_macro2::TokenStream as ProcMacro2TokenStream; use quote::{format_ident, quote}; @@ -13,13 +14,6 @@ use std::io::Read; use std::path::PathBuf; use std::str::FromStr; -/** - * Path to CAN spec JSON files - * Used by all daedalus macros - * Filepath is relative to project root (i.e. /Calypso) - */ -const DAEDALUS_CANGEN_SPEC_PATH: &str = "./Embedded-Base/cangen/can-messages"; - /** * Macro to generate all the code for decode_data.rs * - Generates prelude, phf map, and all decode functions @@ -43,7 +37,7 @@ pub fn gen_decode_data(_item: TokenStream) -> TokenStream { }; let mut __decode_map_entries = ProcMacro2TokenStream::new(); - match fs::read_dir(DAEDALUS_CANGEN_SPEC_PATH) { + match fs::read_dir(CANGEN_SPEC_PATH) { Ok(__entries) => { for __entry in __entries { match __entry { @@ -168,7 +162,7 @@ pub fn gen_encode_data(_item: TokenStream) -> TokenStream { let mut __encode_key_list_entries = ProcMacro2TokenStream::new(); let mut __encode_key_list_size: usize = 0; - match fs::read_dir(DAEDALUS_CANGEN_SPEC_PATH) { + match fs::read_dir(CANGEN_SPEC_PATH) { Ok(__entries) => { for __entry in __entries { match __entry { @@ -330,7 +324,7 @@ pub fn gen_simulate_data(_item: TokenStream) -> TokenStream { }; let mut __simulate_function_body = quote! {}; - match fs::read_dir(DAEDALUS_CANGEN_SPEC_PATH) { + match fs::read_dir(CANGEN_SPEC_PATH) { Ok(__entries) => { for __entry in __entries { match __entry {