import pandas as pd FIELDTYPE_ENUM_FILE = "fieldtype_enum.rs" MESSAGETYPE_ENUM_FILE = "messagetype_enum.rs" MATCH_MESSAGETYPE_FIELD_FILE = "match_messagetype_field.rs" MATCH_MESSAGETYPE_OFFSET_FILE = "match_messagetype_offset.rs" MATCH_MESSAGETYPE_SCALE_FILE = "match_messagetype_scale.rs" MATCH_MESSAGETYPE_FILE = "match_messagetype.rs" MATCH_PREDEFINED_FIELD_VALUE_FILE = "match_predefined_field_value.rs" MATCH_MESSAGE_TIMESTAMP_FIELD_FILE = "match_message_timestamp_field.rs" def camel_case(s): return ''.join(x.title() for x in s.split('_')) def snake_case(s): return s.lower() def shouty_snake_case(s): return snake_case(s).upper() def write_fieldtype_enum(set_ft): f = open("src/" + FIELDTYPE_ENUM_FILE, "w+") f.writelines([ "/// An enum of all possible data types a `Message` field may be\n", "#[derive(Debug, Copy, Clone, PartialEq)]\n", "pub enum FieldType {\n" ]) f.writelines(f" {camel_case(v)},\n" for v in sorted(set_ft)) f.writelines([ " Coordinates,\n" " Timestamp,\n", " None,\n", "}\n" ]) f.close() def write_messagetype_enum(set_mt): f = open("src/" + MESSAGETYPE_ENUM_FILE, "w+") f.writelines([ "/// an enum of all defined messages in the Fit SDK\n", "#[derive(Debug, Copy, Clone, PartialEq)]\n", "pub enum MessageType {\n" ]) f.writelines(f" {camel_case(v)},\n" for v in sorted(set_mt)) f.writelines([ " Pad,\n" " MfgRangeMax,\n", " MfgRangeMin,\n", " None,\n", "}\n", ]) f.close() def write_match_message_field(set_mt, msgs_store): f = open("src/" + MATCH_MESSAGETYPE_FIELD_FILE, "w+") f.writelines([ "use super::{FieldType, MessageType, MatchFieldTypeFn};\n", "\n" ]) for message_type in sorted(set_mt): values = next(x['values'] for x in iter(msgs_store) if x['name'] == message_type) values = sorted(values, key=lambda k: k['id']) f.writelines([ f"fn match_field_{message_type.lower()}(k: usize) -> FieldType {{\n" " match k {\n" ]) f.writelines(f" {m['id']} => FieldType::{camel_case(m['field_type'])},\n" for m in values) f.writelines([ " _ => FieldType::None\n", " }\n" "}\n" ]) f.writelines([ "fn match_field_none(_: usize) -> FieldType {\n", " return FieldType::None;\n", "}\n", ]) f.write(""" /// Determines a specific `FieldType` of any `MessageType`. /// /// The method is called with a `MessageType` argument and returns a static closure /// which is called with a field_id `usize` and yields a `FieldType`. /// Any field that is not defined will return a `FieldType::None` variant. /// /// # Example /// /// ``` /// let message_type = MessageType::WorkoutSession; /// let parsed_value = 3; /// let field_fn = match_message_field(message_type); /// let field = field_fn(parsed_value); /// assert_eq!(field, Field:type::Uint16); /// ``` """) f.writelines([ "pub fn match_message_field(m: MessageType) -> &'static MatchFieldTypeFn {\n", " match m {\n" ]) f.writelines(f" MessageType::{camel_case(v)} => &|x: usize| match_field_{v.lower()}(x),\n" for v in set_mt) f.writelines([ " _ => &|x: usize| match_field_none(x)\n", " }\n", "}\n" ]) f.close() def write_match_message_offset(set_mt, msgs_store): f = open("src/" + MATCH_MESSAGETYPE_OFFSET_FILE, "w+") f.writelines([ "use super::{MessageType, MatchOffsetFn};\n", "\n" ]) for message_type in sorted(set_mt): values = next(x['values'] for x in iter(msgs_store) if x['name'] == message_type) values = sorted(values, key=lambda k: k['id']) f.writelines([ f"fn match_offset_{message_type.lower()}(k: usize) -> Option {{\n", " match k {\n" ]) f.writelines(f" {m['id']} => Some({int(m['offset'])}i16),\n" for m in values if pd.notna(m['offset'])) f.writelines([ " _ => None,\n", " }\n" "}\n" ]) f.writelines([ "fn match_offset_none(_: usize) -> Option {\n", " return None;\n", "}\n", ]) f.write(""" /// Determines whether any SDK-defined `Message` defines an offset for any of its fields. /// /// The method is called with a `MessageType` argument and returns a static closure which is called with a /// field_id `usize` which yields an `Option`. /// /// # Example /// /// ``` /// let message_type = MessageType::Session; /// let parsed_value = 71; /// let offset_fn = match_message_offset(message_type); /// let offset = offset_fn(parsed_value); /// assert_eq!(offset, Some(500.0)); /// ``` """) f.writelines([ "pub fn match_message_offset(m: MessageType) -> &'static MatchOffsetFn {\n", " match m {\n" ]) f.writelines(f" MessageType::{camel_case(v)} => &|x: usize| match_offset_{v.lower()}(x),\n" for v in set_mt) f.writelines([ " _ => &|x: usize| match_offset_none(x)\n", " }\n", "}\n" ]) f.close() def write_match_message_scale(set_mt, msgs_store): f = open("src/" + MATCH_MESSAGETYPE_SCALE_FILE, "w+") f.writelines([ "use super::{MessageType, MatchScaleFn};\n", "\n" ]) for message_type in sorted(set_mt): values = next(x['values'] for x in iter(msgs_store) if x['name'] == message_type) values = sorted(values, key=lambda k: k['id']) f.writelines([ f"fn match_scale_{message_type.lower()}(k: usize) -> Option {{\n", " match k {\n" ]) for m in values: if pd.notna(m['scale']): v = m['scale'] try: v = m['scale'].split(',')[0] except AttributeError: pass f.write(f" {m['id']} => Some({float(v)}f32),\n") f.writelines([ " _ => None,\n", " }\n" "}\n" ]) f.writelines([ "fn match_scale_none(_: usize) -> Option {\n", " return None;\n", "}\n", ]) f.write(""" /// Determines whether any SDK-defined `Message` defines a scale for any of its fields. /// /// The method is called with a `MessageType` argument and returns a static closure which is called with a field_id `usize` /// and yields an `Option`. /// /// # Example /// /// ``` /// let message_type = MessageType::Workout; /// let parsed_value = 14; /// let scale_fn = match_message_scale(message_type); /// let scale = scale_fn(parsed_value); /// assert_eq!(scale, Some(100.0)); /// ``` """) f.writelines([ "pub fn match_message_scale(m: MessageType) -> &'static MatchScaleFn {\n", " match m {\n" ]) f.writelines(f" MessageType::{camel_case(v)} => &|x: usize| match_scale_{v.lower()}(x),\n" for v in set_mt) f.writelines([ " _ => &|x: usize| match_scale_none(x)\n", " }\n", "}\n" ]) f.close() def write_match_messagetype(map): f = open("src/" + MATCH_MESSAGETYPE_FILE, "w+") f.writelines([ "use super::MessageType;\n", "\n", "/// Convert a global_message_id into a `MessageType` enum\n", "pub fn match_messagetype(k: u16) -> MessageType {\n", " match k {\n" ]) f.writelines(f" {k} => MessageType::{camel_case(v)},\n" for k, v in map.items()) f.writelines([ " _ => MessageType::None\n", " }\n", "}\n" ]) f.close() def write_match_predefined_field_value(set_f, types_store): f = open("src/" + MATCH_PREDEFINED_FIELD_VALUE_FILE, "w+") f.writelines([ "use super::FieldType;\n", "\n", ]) f.write(""" /// Certain `FieldType` values refer to predefined text strings in the SDK. /// /// # Example /// /// ``` /// let field_type = FieldType::BodyLocation; /// let parsed_value = 27; /// let predefined_text = match_predefined_field_value(field_type, parsed_value); /// assert_eq!(predefined_text, Some("left_forearm_extensors")); /// ``` """) f.writelines([ "pub fn match_custom_field_value(f: FieldType, k: usize) -> Option<&'static str> {\n", " match f {\n" ]) for ft in sorted(set_f): try: map = next(x['values'] for x in iter(types_store) if x['name'] == ft) f.write(f" FieldType::{camel_case(ft)} => match k {{\n") f.writelines(f" {int(k, 0)} => Some(\"{v}\"),\n" for k, v in map.items()) f.writelines([ " _ => None,\n", " }\n" ]) except StopIteration: pass f.writelines([ " FieldType::None => None,\n", " _ => None\n", " }\n", "}\n" ]) f.close() def write_match_message_timestamp_field(set_mt, msgs_store): f = open("src/" + MATCH_MESSAGE_TIMESTAMP_FIELD_FILE, "w+") f.writelines([ "use super::MessageType;\n", "\n", "/// A method for obtaining the field ID a specified `MessageType` uses for its timestamp. Usually it's 253 but unfortunately not always.\n", "pub fn match_message_timestamp_field(mt: MessageType) -> Option {\n", " match mt {\n" ]) for mt in sorted(set_mt): try: values = next(x['values'] for x in iter(msgs_store) if x['name'] == mt) ts = next(x['id'] for x in iter(values) if x['field_type'] == "Timestamp") f.write(f" MessageType::{camel_case(mt)} => Some({ts}),\n") except StopIteration: pass f.writelines([ " _ => None\n", " }\n", "}\n" ]) f.close() df = pd.read_excel('Profile.xlsx', sheet_name='Types') types_list = [] elem = {'values': {}} for row in df.itertuples(): if pd.notna(row[1]) and pd.notna(row[2]): if 'name' in elem: types_list.append(elem) elem = {'values': {}} elem['name'] = row[1] elem['base_type'] = row[2] elif pd.notna(row[3]) and pd.notna(row[4]): elem['values'][str(row[4])] = row[3] types_list.append(elem) # read messages df = pd.read_excel('Profile.xlsx', sheet_name='Messages') msgs_list = [] msg = {'values': []} fields_set = set([]) for row in df.itertuples(): if pd.notna(row[1]): if 'name' in msg: msgs_list.append(msg) msg = {'values': []} msg['name'] = row[1] elif pd.notna(row[2]) and pd.notna(row[3]): fields_set.add(row[4]) if row[3].endswith("_lat") or row[3].endswith("_long"): val = "Coordinates" elif row[3].endswith("timestamp"): val = "Timestamp" else: val = row[4] r = {'id': int(row[2]), 'field_name': row[3], 'field_type': val, 'scale': row[7], 'offset': row[8]} msg['values'].append(r) msgs_list.append(msg) msg_types = next(x for x in iter(types_list) if x['name'] == 'mesg_num') msgs = [x['name'] for x in msgs_list] write_fieldtype_enum(fields_set) write_messagetype_enum(msgs) write_match_message_field(msgs, msgs_list) write_match_message_offset(msgs, msgs_list) write_match_message_scale(msgs, msgs_list) write_match_messagetype(msg_types['values']) write_match_predefined_field_value(fields_set, types_list) write_match_message_timestamp_field(msgs, msgs_list)