use calamine::{open_workbook, DataType, Range, Reader, Xlsx};
use std::collections::{BTreeMap, HashSet};
use std::env;
use std::fs::File;
use std::io::prelude::*;
use std::process::Command;
#[derive(Clone, Debug)]
pub struct FitProfile {
field_types: Vec<FieldTypeDefintion>,
messages: Vec<MessageDefinition>,
}
#[derive(Clone, Debug)]
struct FieldTypeDefintion {
name: String,
titlized_name: String,
base_type: &'static str,
variant_map: BTreeMap<i64, FieldTypeVariant>,
}
impl FieldTypeDefintion {
fn new(name: String, base_type: &'static str) -> Self {
FieldTypeDefintion {
name: name.clone(),
titlized_name: titlecase_string(&name),
base_type,
variant_map: BTreeMap::new(),
}
}
fn generate_enum(&self, out: &mut File) -> Result<(), std::io::Error> {
write!(out, "#[derive(Clone, Copy, Debug)]\n")?;
write!(out, "pub enum {} {{\n", titlecase_string(&self.name))?;
for variant in self.variant_map.values() {
variant.write_variant_line(out)?;
}
write!(out, "UnknownVariant({}),\n", self.base_type)?;
write!(out, "}}\n")?;
self.generate_impl(out)
}
fn generate_impl(&self, out: &mut File) -> Result<(), std::io::Error> {
write!(out, "impl {} {{\n", titlecase_string(&self.name))?;
write!(
out,
"pub fn from_{0}(value: {0}) -> {1} {{\n",
self.base_type,
titlecase_string(&self.name)
)?;
write!(out, "match value {{\n")?;
for variant in self.variant_map.values() {
write!(
out,
"{} => {}::{},\n",
variant.value,
titlecase_string(&self.name),
variant.titlized_name()
)?;
}
write!(
out,
" _ => {}::UnknownVariant(value)\n",
titlecase_string(&self.name)
)?;
write!(out, "}}\n")?;
write!(out, "}}\n")?;
write!(
out,
"pub fn from_i64(value: i64) -> {} {{\n",
titlecase_string(&self.name)
)?;
write!(
out,
"{0}::from_{1}(value as {1})\n",
titlecase_string(&self.name),
self.base_type
)?;
write!(out, "}}\n")?;
write!(out, "pub fn as_{0}(&self) -> {0} {{\n", self.base_type)?;
write!(out, "match &self {{\n")?;
for variant in self.variant_map.values() {
write!(
out,
"{}::{} => {},\n",
titlecase_string(&self.name),
variant.titlized_name(),
variant.value
)?;
}
write!(
out,
"{}::UnknownVariant(value) => *value\n",
titlecase_string(&self.name)
)?;
write!(out, "}}\n")?;
write!(out, "}}\n")?;
write!(out, "pub fn as_i64(&self) -> i64 {{\n")?;
write!(out, "self.as_{}() as i64\n", self.base_type)?;
write!(out, "}}\n")?;
write!(out, "pub fn to_string(&self) -> String {{\n")?;
write!(out, "match &self {{\n")?;
for variant in self.variant_map.values() {
write!(
out,
"{}::{} => \"{}\".to_string(),\n",
titlecase_string(&self.name),
variant.titlized_name(),
variant.name
)?;
}
write!(
out,
"{}::UnknownVariant(value) => format!(\"unknown_variant_{{}}\", *value)\n",
titlecase_string(&self.name)
)?;
write!(out, "}}\n")?;
write!(out, "}}\n")?;
write!(out, "}}\n\n")?;
Ok(())
}
}
#[derive(Clone, Debug)]
struct FieldTypeVariant {
name: String,
value: i64,
comment: Option<String>,
}
impl FieldTypeVariant {
fn titlized_name(&self) -> String {
let mut titlized_name = titlecase_string(&self.name);
let first_let = titlized_name.as_bytes()[0];
if first_let < 65 || first_let > 90 {
titlized_name = format!("Name{}", titlized_name);
}
titlized_name
}
fn write_variant_line(&self, out: &mut File) -> Result<(), std::io::Error> {
if let Some(v) = &self.comment {
write!(out, "{}, // {}\n", self.titlized_name(), v)
} else {
write!(out, "{},\n", self.titlized_name())
}
}
}
#[derive(Clone, Debug)]
struct MessageDefinition {
name: String,
field_map: BTreeMap<u8, MessageFieldDefinition>,
}
impl MessageDefinition {
fn new(name: &str) -> Self {
MessageDefinition {
name: name.to_string(),
field_map: BTreeMap::new(),
}
}
fn get_field_by_name(&self, name: &str) -> &MessageFieldDefinition {
for field in self.field_map.values() {
if field.name == name {
return field;
}
}
panic!(format!("No field with name: {:?}", name));
}
fn function_name(&self) -> String {
format!("{}_message", self.name)
}
fn write_function_def(&self, out: &mut File) -> Result<(), std::io::Error> {
write!(
out,
"fn {}(global_message_number: u16) -> MessageInfo {{\n",
self.function_name()
)?;
write!(out, " let mut fields = HashMap::new();\n\n")?;
for field in self.field_map.values() {
field.generate_field_info_struct(out, &self, "field")?;
write!(out, "fields.insert({}, field);\n\n", field.def_number)?;
}
write!(out, " MessageInfo {{\n")?;
write!(out, " name: \"{}\",\n", self.name)?;
write!(out, " global_message_number,\n")?;
write!(out, " fields: fields\n")?;
write!(out, " }}\n")?;
write!(out, "}}\n\n")?;
Ok(())
}
}
#[derive(Clone, Debug)]
struct MessageFieldDefinition {
def_number: u8,
name: String,
field_type: String,
scale: f64,
offset: f64,
units: String,
accumulate: bool,
subfields: Vec<(String, String, MessageFieldDefinition)>,
components: Vec<MessageFieldComponent>,
comment: Option<String>,
}
impl MessageFieldDefinition {
pub fn generate_field_info_struct(
&self,
out: &mut File,
mesg: &MessageDefinition,
var_name: &str,
) -> Result<(), std::io::Error> {
let subfield_var: &'static str;
if self.subfields.is_empty() {
subfield_var = "Vec::new()";
} else {
subfield_var = "subfields";
write!(out, "let mut {} = Vec::new();\n", subfield_var)?;
for (fld_name, fld_value, sub_info) in &self.subfields {
sub_info.generate_field_info_struct(out, mesg, "sub_fld")?;
let ref_field = mesg.get_field_by_name(fld_name);
write!(
out,
"subfields.push(({}, {}::{}.as_i64(), {}));\n",
ref_field.def_number,
ref_field.field_type,
titlecase_string(fld_value),
"sub_fld"
)?;
}
}
let components_var: &'static str;
if self.components.is_empty() {
components_var = "Vec::new()";
} else {
components_var = "components";
write!(out, "let mut {} = Vec::new();\n", components_var)?;
for comp_info in &self.components {
comp_info.generate_comp_field_info_struct(out, mesg, "comp_fld")?;
write!(out, "components.push(comp_fld);\n")?;
}
}
if let Some(v) = &self.comment {
write!(out, "// {}\n", v)?;
}
write!(
out,
" let {} = FieldInfo {{
name: \"{}\",
field_type: FieldDataType::{},
def_number: {},
scale: {:.6},
offset: {:.6},
units: \"{}\",
accumulate: {},
subfields: {},
components: {},
}};\n",
var_name,
self.name,
self.field_type,
self.def_number,
self.scale,
self.offset,
self.units,
self.accumulate,
subfield_var,
components_var
)?;
Ok(())
}
}
#[derive(Clone, Debug)]
struct MessageFieldComponent {
name: String,
scale: f64,
offset: f64,
units: String,
bits: u8,
accumulate: bool,
}
impl MessageFieldComponent {
fn generate_comp_field_info_struct(
&self,
out: &mut File,
mesg: &MessageDefinition,
var_name: &str,
) -> Result<(), std::io::Error> {
let dest_def_number = mesg.get_field_by_name(&self.name).def_number;
write!(
out,
"let {} = ComponentFieldInfo {{
dest_def_number: {},
scale: {:.6},
offset: {:.6},
units: \"{}\",
bits: {},
accumulate: {},
}};\n",
var_name,
dest_def_number,
self.scale,
self.offset,
self.units,
self.bits,
self.accumulate,
)?;
Ok(())
}
}
fn titlecase_string(value: &str) -> String {
let mut words: Vec<String> = value.split('_').map(|v| v.to_string()).collect();
for word in &mut words {
if let Some(l) = word.get_mut(0..1) {
l.make_ascii_uppercase();
}
}
String::from(words.join(""))
}
macro_rules! split_csv_string ( ($value:expr) => ( {$value.split(',').map(|v| v.trim().to_string())} ););
fn base_type_to_rust_type(base_type_str: &str) -> &'static str {
match base_type_str {
"enum" => "u8",
"sint8" => "i8",
"uint8" => "u8",
"uint8z" => "u8",
"sint16" => "i16",
"uint16" => "u16",
"uint16z" => "u16",
"sint32" => "i32",
"uint32" => "u32",
"uint32z" => "u32",
_ => panic!(format!(
"unsupported base_type for enum field: {}",
base_type_str
)),
}
}
fn field_type_str_to_field_type(field_type_str: &str) -> String {
match field_type_str {
"sint8" => "SInt8".to_string(),
"uint8" => "UInt8".to_string(),
"sint16" => "SInt16".to_string(),
"uint16" => "UInt16".to_string(),
"sint32" => "SInt32".to_string(),
"uint32" => "UInt32".to_string(),
"string" => "String".to_string(),
"float32" => "Float32".to_string(),
"float64" => "Float64".to_string(),
"uint8z" => "UInt8z".to_string(),
"uint16z" => "UInt16z".to_string(),
"uint32z" => "UInt32z".to_string(),
"byte" => "Byte".to_string(),
"sint64" => "SInt64".to_string(),
"uint64" => "UInt64".to_string(),
"uint64z" => "UInt64z".to_string(),
_ => titlecase_string(field_type_str),
}
}
fn generate_main_field_type_enum(
field_types: &[FieldTypeDefintion],
out: &mut File,
) -> Result<(), std::io::Error> {
let base_types = vec![
"Bool", "SInt8", "UInt8", "SInt16", "UInt16", "SInt32", "UInt32", "String", "Float32",
"Float64", "UInt8z", "UInt16z", "UInt32z", "Byte", "SInt64", "UInt64", "UInt64z",
];
let mut is_enum_force_false = HashSet::new();
is_enum_force_false.insert("date_time".to_string());
is_enum_force_false.insert("local_date_time".to_string());
write!(
out,
"
/// Describe all possible data types of a field
///
/// The Enum type's value is actually an enum of enums.
#[derive(Clone, Copy, Debug)]
pub enum FieldDataType {{
"
)?;
for type_name in base_types {
write!(out, " {},\n", type_name)?;
}
for field_type in field_types {
write!(out, "{},\n", field_type.titlized_name)?;
}
write!(out, "}}\n\n")?;
write!(out, "impl FieldDataType {{\n")?;
write!(out, " pub fn is_enum_type(&self) -> bool {{\n")?;
write!(out, " match self {{\n")?;
for field_type in field_types {
if !field_type.variant_map.is_empty() && !is_enum_force_false.contains(&field_type.name) {
write!(
out,
"FieldDataType::{} => true,\n",
field_type.titlized_name
)?;
}
}
write!(out, " _ => false,\n")?;
write!(out, " }}\n")?;
write!(out, " }}\n")?;
write!(out, "}}\n")?;
write!(
out,
"pub fn get_field_variant_as_string(field_type: FieldDataType , value: i64) -> String {{\n"
)?;
write!(out, " match field_type {{\n")?;
for field_type in field_types {
if !field_type.variant_map.is_empty() && !is_enum_force_false.contains(&field_type.name) {
write!(
out,
" FieldDataType::{0} => {0}::from_i64(value).to_string(),\n",
field_type.titlized_name
)?;
}
}
write!(out, " _ => format!(\"Undefined{{}}\", value),\n")?;
write!(out, " }}\n")?;
write!(out, "}}\n")?;
Ok(())
}
fn process_types(sheet: Range<DataType>) -> Vec<FieldTypeDefintion> {
let mut field_types: Vec<FieldTypeDefintion> = Vec::new();
for row in sheet.rows().skip(1) {
if !row[0].is_empty() {
let enum_name = match row[0].get_string() {
Some(v) => v.to_string(),
None => panic!(format!("Enum type name must be a string row={:?}.", row)),
};
let rust_type = match row[1].get_string() {
Some(v) => base_type_to_rust_type(v),
None => panic!(format!("Base type name must be a string row={:?}.", row)),
};
field_types.push(FieldTypeDefintion::new(enum_name, rust_type));
} else if !row[2].is_empty() {
let field_type = match field_types.last_mut() {
Some(v) => v,
None => panic!("field_types vector was empty!"),
};
let name = match row[2].get_string() {
Some(v) => v.to_string(),
None => panic!(format!("Enum variant name must be a string row={:?}.", row)),
};
let value = match &row[3] {
DataType::Float(v) => *v as i64,
DataType::Int(v) => *v,
DataType::String(v) => i64::from_str_radix(&v[2..], 16).unwrap(),
_ => {
panic!(format!(
"Unsupported enum variant value data type row={:?}.",
row
));
}
};
let comment = match row[4].get_string() {
Some(v) => Some(v.to_string()),
None => None,
};
field_type.variant_map.insert(
value,
FieldTypeVariant {
name,
value,
comment,
},
);
}
}
field_types
}
fn parse_message_field_components(row: &[DataType]) -> Vec<MessageFieldComponent> {
let mut components = Vec::new();
let names: Vec<String> = match row[5].get_string() {
Some(v) => split_csv_string!(v).collect(),
None => {
return components;
}
};
let cols: Vec<String> = row[6..=10].into_iter().map(|v| v.to_string()).collect();
let mut scales = split_csv_string!(cols[0]).map(|s| s.parse::<f64>().ok());
let mut offsets = split_csv_string!(cols[1]).map(|s| s.parse::<f64>().ok());
let mut units = split_csv_string!(cols[2]);
let mut bits = split_csv_string!(cols[3]).map(|s| s.parse::<u8>().ok());
let mut accumulate = split_csv_string!(cols[4]).map(|s| s == "1");
for name in names {
components.push(MessageFieldComponent {
name,
scale: scales.next().flatten().unwrap_or(1.0),
offset: offsets.next().flatten().unwrap_or(0.0),
units: units.next().unwrap_or(String::new()),
bits: bits
.next()
.flatten()
.expect(&format!("Could not parse bits value for row: {:?}", row)),
accumulate: accumulate.next().unwrap_or(false),
});
}
components
}
fn new_message_field_definition(row: &[DataType]) -> MessageFieldDefinition {
let def_number = match row[1] {
DataType::Float(v) => v as u8,
DataType::Int(v) => v as u8,
_ => panic!(format!(
"Field defintiton number must be an integer, row={:?}.",
row
)),
};
let name = row[2]
.get_string()
.expect(&format!("Field name must be a string, row={:?}.", row));
let ftype = row[3]
.get_string()
.expect(&format!("Field type must be a string, row={:?}.", row));
let components = parse_message_field_components(&row);
let comment = match row[13].get_string() {
Some(v) => Some(v.to_string()),
None => None,
};
MessageFieldDefinition {
def_number,
name: name.to_string(),
field_type: field_type_str_to_field_type(ftype),
scale: row[6].get_float().unwrap_or(1.0),
offset: row[7].get_float().unwrap_or(0.0),
units: row[8].get_string().unwrap_or("").to_string(),
accumulate: row[10].to_string() == "1",
subfields: Vec::new(),
components,
comment,
}
}
fn process_messages(sheet: Range<DataType>) -> Vec<MessageDefinition> {
let mut rows = sheet.rows().skip(2);
let mut messages: Vec<MessageDefinition> = Vec::new();
let mut msg: MessageDefinition;
let mut field: MessageFieldDefinition;
let mut last_def_number: u8 = 0;
let row = rows.next().unwrap();
if let Some(v) = row[0].get_string() {
msg = MessageDefinition::new(v);
} else {
panic!(format!("Message name must be a string row={:?}.", row));
}
for row in rows {
if !row[0].is_empty() {
if let Some(v) = row[0].get_string() {
messages.push(msg);
msg = MessageDefinition::new(v);
} else {
panic!(format!("Message name must be a string row={:?}.", row));
}
} else if !row[1].is_empty() {
field = new_message_field_definition(row);
last_def_number = field.def_number;
msg.field_map.insert(field.def_number, field);
} else if !row[2].is_empty() {
let parent = msg
.field_map
.get_mut(&last_def_number)
.expect("No parent field defined for subfield!");
let mut temp_row: Vec<DataType> = Vec::from(row);
temp_row[1] = DataType::Int(last_def_number as i64);
field = new_message_field_definition(&temp_row);
let ref_field_names = row[11].get_string().expect("No reference field name(s)");
let ref_field_vals = row[12].get_string().expect("No reference field value(s)");
for (name, value) in
split_csv_string!(ref_field_names).zip(split_csv_string!(ref_field_vals))
{
parent.subfields.push((name, value, field.clone()));
}
}
}
messages.push(msg);
messages
}
pub fn parse_profile(profile_fname: &str) -> Result<FitProfile, Box<dyn std::error::Error>> {
let mut excel: Xlsx<_> = open_workbook(&profile_fname)?;
let field_types = if let Some(Ok(sheet)) = excel.worksheet_range("Types") {
process_types(sheet)
} else {
panic!("Could not access workbook sheet 'Types'");
};
let messages = if let Some(Ok(sheet)) = excel.worksheet_range("Messages") {
process_messages(sheet)
} else {
panic!("Could not access workbook sheet 'Messages'");
};
Ok(FitProfile {
field_types,
messages,
})
}
fn create_mesg_num_to_mesg_info_fn(
messages: &Vec<MessageDefinition>,
out: &mut File,
) -> Result<(), std::io::Error> {
write!(out, "impl MesgNum {{\n")?;
write!(out, " pub fn message_info(&self) -> MessageInfo {{\n")?;
write!(out, " match self {{\n")?;
for msg in messages {
write!(
out,
" MesgNum::{} => {}(self.as_u16()),\n",
titlecase_string(&msg.name),
msg.function_name()
)?;
}
write!(out, " _ => unknown_message(self.as_u16()),\n")?;
write!(out, " }}\n")?;
write!(out, " }}\n")?;
write!(out, "}}\n")?;
Ok(())
}
fn rustfmt(fname: &str) {
Command::new("rustfmt")
.arg(&fname)
.status()
.expect(&format!("failed to execute rustfmt on {}", fname));
}
fn write_types_files(profile: &FitProfile) -> Result<(), std::io::Error> {
let fname = "src/profile/field_types.rs";
let mut out = File::create(&fname)?;
write!(out, "#![allow(missing_docs)]\n")?;
write!(out, "#![allow(dead_code)]\n")?;
write!(
out,
"//! Auto generated profile field types from FIT SDK Release: XXX\n"
)?;
write!(
out,
"//! Not all of these may be used by the defined set of FIT messages\n\n"
)?;
for field_type in &profile.field_types {
if !field_type.variant_map.is_empty() {
field_type.generate_enum(&mut out)?;
}
}
generate_main_field_type_enum(&profile.field_types, &mut out)?;
rustfmt(&fname);
Ok(())
}
fn write_messages_file(profile: &FitProfile) -> Result<(), std::io::Error> {
let fname = "src/profile/messages.rs";
let mut out = File::create(&fname)?;
write!(out, "#![allow(missing_docs)]\n")?;
write!(
out,
"//! Auto generated profile messages from FIT SDK Release: XXX\n\n"
)?;
write!(out, "use std::collections::HashMap;\n")?;
write!(
out,
"use super::{{ComponentFieldInfo, FieldDataType, FieldInfo, MessageInfo}};\n"
)?;
write!(out, "use super::field_types::*;\n\n")?;
for msg in &profile.messages {
msg.write_function_def(&mut out)?;
}
write!(
out,
"fn unknown_message(global_message_number: u16) -> MessageInfo {{\n"
)?;
write!(out, " MessageInfo {{\n")?;
write!(out, " name: \"unknown\",\n")?;
write!(out, " global_message_number,\n")?;
write!(out, " fields: HashMap::new()\n")?;
write!(out, " }}\n")?;
write!(out, "}}\n\n")?;
create_mesg_num_to_mesg_info_fn(&profile.messages, &mut out)?;
rustfmt(&fname);
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-env-changed=FIT_PROFILE");
let profile_fname = match env::var("FIT_PROFILE") {
Ok(val) => {
eprintln!("Reading FIT profile at {}", &val);
val
}
Err(_) => {
println!("cargo:warning=Did not update FIT profile, could not read FIT_PROFILE environment variable");
return Ok(());
}
};
let profile = parse_profile(&profile_fname).unwrap();
write_types_files(&profile)?;
write_messages_file(&profile)?;
Ok(())
}