reading fit files

http://www.thisisant.com/pages/products/fit-sdk

The Flexible and Interoperable Data Transfer (FIT) protocol is designed specifically for the storing and sharing of data that originates from sport, fitness and health devices. The FIT protocol defines a set of data storage templates (FIT messages) that can be used to store information such as user profiles and activity data in files. It is specifically designed to be compact, interoperable and extensible.


 namespace eval ::fit {}

 array set ::fit::global_msg {
  0 file_id
  1 capabilities
  2 device_settings
  3 user_profile
  4 hrm_profile
  5 sdm_profile
  6 bike_profile
  7 zones_target
  8 hr_zone
  9 power_zone
  10 met_zone
  12 sport
  15 goal
  18 session
  19 lap
  20 record
  21 event
  23 device_info
  26 workout
  27 workout_step
  30 weight_scale
  31 course
  32 course_point
  33 totals
  34 activity
  35 software
  37 file_capabilities
  38 mesg_capabilities
  39 field_capabilities
  49 file_creator
  51 blood_pressure
 }

 array set ::fit::msg_activity {
  253 timestamp
  0 total_timer_time
  1 num_sessions
  2 type
  3 event
  4 event_type
  5 local_timestamp
  6 event_group
 }

 array set ::fit::msg_session {
  254 message_index
  253 timestamp
  0 event
  1 event_type
  2 start_time
  3 start_position_lat
  4 start_position_long
  5 sport
  6 sub_sport
  7 total_elapsed_time
  8 total_timer_time
  9 total_distance
  10 total_cycles
  11 total_calories
  13 total_fat_calories
  14 avg_speed
  15 max_speed
  16 avg_heart_rate
  17 max_heart_rate
  18 avg_cadence
  19 max_cadence
  20 avg_power
  21 max_power
  22 total_ascent
  23 total_descent
  24 total_training_effect
  25 first_lap_index
  26 num_laps
  27 event_group
  28 trigger
  29 nec_lat
  30 nec_long
  31 swc_lat
  32 swc_long
 }

 array set ::fit::msg_record {
  253 timestamp
  0 position_lat
  1 position_long
  2 altitude
  3 heart_rate
  4 cadence
  5 distance
  6 speed
  7 power
  8 compressed_speed_distance
  9 grade
  10 resistance
  11 time_from_course
  12 cycle_length
  13 temperature
 }

 array set ::fit::msg_file_id {
  0 type
  1 manufacturer
  2 product
  3 serial_number
  4 time_created
  5 number
 }

 array set ::fit::msg_file_creator {
  0 software_version
  1 hardware_version
 }

 array set ::fit::msg_event {
  253 timestamp
  0 event
  1 event_type
  2 data16
  3 data
  4 event_group
 }

 array set ::fit::msg_device_info {
  253 timestamp
  0 device_index
  1 device_type
  2 manufacturer
  3 serial_number
  4 product
  5 software_version
  6 hardware_version
  7 cum_operating_time
  10 battery_voltage
  11 battery_status
 }

 array set ::fit::msg_lap {
  254 message_index
  253 timestamp
  0 event
  1 event_type
  2 start_time
  3 start_position_lat
  4 stat_position_long
  5 end_position_lat
  6 end_position_long
  7 total_elapsed_time
  8 total_timer_time
  9 total_distance
  10 total_cycles
  11 total_calories
  12 total_fat_calories
  13 avg_speed
  14 max_speed
  15 avg_heart_rate
  16 max_heart_rate
  17 avg_cadence
  18 max_cadence
  19 avg_power
  20 max_power
  21 total_ascent
  22 total_descent
  23 intensity
  24 lap_trigger
  25 sport
  26 event_group
 }

 proc ::fit::read_header {fh} {
  binary scan [read $fh 12] ccsia4 hlen proto profile size magic
  if {$magic != ".FIT"} {
    return -code error "not a fit file"
  }
  seek $fh $hlen start
  return [dict create \
    header_len $hlen \
    data_size $size \
    proto $proto \
    profile $profile \
  ]
 }

 proc ::fit::decode_definition {fh} {
  variable global_msg
  set len 0
  set fields [list]
  binary scan [read $fh 5] ccsuc res arch msg cnt
  if {[info exists global_msg($msg)]} { set msg $global_msg($msg) }
    for {set i 0} {$i < $cnt} {incr i} {
      binary scan [read $fh 3] cuccu field size base
      incr len $size
      lappend fields $field $size $base
  }
  return [dict create \
    global_msg $msg \
    count $cnt \
    len $len \
    fields $fields \
  ]
 }

 proc ::fit::read_record {fh} {
  variable msg_defs
  variable global_msg
  binary scan [read $fh 1] c header
  set ret {}
  if {$header >> 7 > 0} {
    # compressed timestamp header
    dict set ret record data
    set type [expr {($header & 0b01111111) >> 5}]
    if {![info exists msg_defs($type)]} {
      return -code error "undefined message type $type"
    }
    dict set ret type [dict get $msg_defs($type) global_msg]
    set raw [read $fh [dict get $msg_defs($type) len]]
    dict set ret data [decode_data_fields [dict get $msg_defs($type) global_msg] $raw [dict get $msg_defs($type) fields]]
    dict set ret data relative_timestamp [expr {$header & 0b00011111}]
  } else {
    # normal header
    set type [expr {$header & 0b00001111}]
    if {$header & 0b01000000} {
      set def [decode_definition $fh]
      set msg_defs($type) $def
      dict set ret record definition
      dict set ret local_type $type
      dict set ret definition $def
    } else {
      dict set ret record data
      if {![info exists msg_defs($type)]} {
        return -code error "undefined message type $type"
      }
      dict set ret type [dict get $msg_defs($type) global_msg]
      set raw [read $fh [dict get $msg_defs($type) len]]
      dict set ret data [decode_data_fields [dict get $msg_defs($type) global_msg] $raw [dict get $msg_defs($type) fields]]
    }
  }
  return $ret
 }

 proc ::fit::decode_data_fields {msg data fields} {
  set ret {}
  variable msg_$msg
  upvar 0 msg_$msg msgs
  set offset 0
  foreach {name len datatype} $fields {
    if {![info exists msgs($name)]} {
      dict set ret $name {}
      continue
    }
    switch -- $datatype {
      0   { binary scan $data x${offset}a val }
      1   { binary scan $data x${offset}c val }
      2   { binary scan $data x${offset}cu val }
      131 { binary scan $data x${offset}s val }
      132 { binary scan $data x${offset}su val }
      133 { binary scan $data x${offset}i val }
      134 { binary scan $data x${offset}iu val }
      7   { binary scan $data x${offset}a$len val }
      135 { binary scan $data x${offset}f val }
      137 { binary scan $data x${offset}cu val }
      10  { binary scan $data x${offset}cu val }
      139 { binary scan $data x${offset}su val }
      140 { binary scan $data x${offset}iu val }
      14  { binary scan $data x${offset}a$len val }
      default { return -code error "unknown data type $datatype" }
    }
    dict set ret $msgs($name) $val
    incr offset $len
  }
  return $ret
 }

 array set ::fit::cook_imperial {
  timestamp {expr {$val + 631065600}}
  position_lat {expr {$val / (2**31 / 180.00)}}
  position_long {expr {$val / (2**31 / 180.00)}}
  speed {expr {($val / 1609344.00) * 3600}}
  altitude {expr {(($val / 5.00) - 500) * 3.2808399}}
 }

 proc ::fit::format_values {rec {sys imperial}} {
  if {$sys != "imperial" && $sys != "metric"} { return -code error "measurement system must be either metric or imperial" }
  variable cook_$sys
  upvar 0 cook_$sys cook
  if {[dict get $rec record] != "data"} { return $rec }
  dict for {key val} [dict get $rec data] {
    if {[info exists cook([dict get $rec type]_$key)]} {
      dict set rec data $key [eval $cook([dict get $rec type]_$key)]
    } elseif {[info exists cook($key)]} {
      dict set rec data $key [eval $cook($key)]
    }
  }
  return $rec
 }

 proc ::fit::fit_data {file} {
  variable msg_defs
  unset -nocomplain msg_defs
  set fh [open $file r]
  fconfigure $fh -translation binary
  set header [read_header $fh]
  set out [list]
  set data_end [expr [dict get $header header_len] + [dict get $header data_size]]
  while {[tell $fh] < $data_end} {
    set rec [read_record $fh]
    if {[dict get $rec record] == "data"} {
      lappend out [format_values $rec]
    }
  }
  return $out
 }