サイコンのFitファイルを解析する

thumbnail for this post

僕はレザインのサイクルコンピュータを使っているのですが、バックアップを取ると、.fitという拡張子でGPSのログが取られていることがわかります。
これは中身解析したくなりますよね?!

.fitファイルの仕様は、antのサイトで見つかりました。
Web Software Tools - THIS IS ANT
ここのFIT SDKから入手

.fitファイルの中身

この記事はサイクルコンピュータのファイル限定で書くので、フィットネス用の時計とかだと中に含まれる情報がちょっと違うと思います。
基本の解析方法は変わりませんので参考にはなると思います。

Fitファイルの構造は以下のようになります。

  • ファイルヘッダ: 14バイト
  • データヘッダ: 1バイト
    • (データヘッダ & 0x40)!=0 の時は「レコード定義」が続く
      レコード定義IDとして"データヘッダ^0x40"を使う
    • (データヘッダ & 0x40)==0 の時は「定義に基づいたデータ」が続く
      レコード定義IDはこのデータヘッダの^0x40
  • データ(レコード定義 または 定義に基づいたデータ)

以下、定義かデータが繰り返される

例えば、

フィールド 内容
ファイルヘッダ: 14バイト バージョンとか。使わないかな?
データヘッダ: 0x42 0x42 ^ 0x40 !=0なので、レコード定義が続く、レコード定義IDは0x42^0x40 = 0x02
レコード定義 ID 0x02の定義データ
データヘッダ: 0x02 0x02 ^ 0x40 ==0なので、レコードデータが続く、レコード定義IDは0x02^0x40 = 0x02
レコード ID 0x02のデータ
データヘッダ: 0x02 上の0x02と同様。特定IDのレコード定義は一度だけ、データは何度も現れる。
レコード ID 0x02のデータ
データヘッダ: 0x47 0x47 ^ 0x40 !=0なので、レコード定義が続く、レコード定義IDは0x47^0x40 = 0x07
レコード定義 ID 0x07の定義データ
データヘッダ: 0x02 上の0x02と同様
レコード ID 0x02のデータ
データヘッダ: 0x07 0x07 ^ 0x40 ==0なので、レコードデータが続く、レコード定義IDは0x07^0x40 = 0x07
レコード ID 0x07のデータ

という感じです。

ファイルヘッダの構造

これは、コード見たほうが早いです。
(名称,pack/unpackする時の文字,バイト数)

    @header_structure = [
      [:header_size , 'C',1],
      [:protocol_version, 'C',1],
      [:profile_version, 'v',2],
      [:data_size, 'V',4],
      [:data_type_byte, 'V',4],
      [:crc, 'v',2],
    ]

基本、ヘッダサイズ分だけ読み飛ばせば良いと思います。

レコード定義の構造

データヘッダ^0x40が0ではない時(7bit目が立っている時)にはレコードの定義が続きます。
ちょっとややこしい構造をしています。

  • レコードヘッダ:一個
  • レコードの各フィールドの定義: フィールドの個数分だけ。

という2段構成になっています。

    @record_header_structure = [
      [:reserved, 'C',1],
      [:architecture, 'C',1], #0:little endian, 1:big endian. レザインはlittle endianだった
      [:global_message_number, 'v',2], 
      [:fields, 'c',1],
    ]
レコードヘッダで重要なのはグローバルメッセージ番号(global message number)と、フィールド数(fields)です。
グローバルメッセージ番号は、そのレコードが何を意味するか一意に決まる番号で、仕様書の中にあるエクセルファイルで定義されています。

例えば、僕のとある日のサイコンのfitファイルのグローバルメッセージの番号と、そのレコード数は、

名称 Global Message Number レコード数
file_id 0 1 record
file_creator 49 1
timestamp_correlation 162 1
device_info 23 3
record 20 2351
event 21 3
lap 19 3
session 18 1
activity 34 1

上の表でわかるように、使うのは"record: id=20"がメインになると思います。

例えば、レコードヘッダが、:global_message_number = 20, :fields = 5となっていた場合、record(id:20)には5つのデータフィールドがあることを示しています。(緯度、経度、時間など)

なので、:fieldsの数だけフィールド定義を読み込むことになります。

各フィールドの定義は、以下のようになっているので、これを:fieldsの数だけ読み込みます。

    @field_definition = [
      [:field_definition_number, 'C',1],
      [:size, 'C',1], #byte
      [:base_type, 'C',1],
    ]

ここでフィールド定義番号(field definition number)も、またエクセルで定義されてます。
しかも、グローバルメッセージ番号毎に定義が異なります。。。

record(id:20)の場合は、以下のようになります。
ついでに、値を人の目で見てわかるように変換するコードもくっついてます。

      {
        253 => {:name => :timestamp, :func => lambda{|value| Time.at(628473600 + value).to_s } },
         0 => {:name => :position_lat, :func => lambda{|value| (value == 0x7fffffff)? "na" : value * ( 180.0 / (2**31) ) } }, # semicircles to degree
         1 => {:name => :position_long, :func => lambda{|value|(value == 0x7fffffff)? "na" : value * ( 180.0 / (2**31) ) } },
         5 => {:name => :distance, :func => lambda {|value| (value == 0xffffffff)? "na" : value/100.0}},
         29 => {:name => :accumulated_power, :func => lambda{|value| (value == 0xffffffff)? "na" : value}},
         2 => {:name => :altitude, :func => lambda{|value| (value == 0xffff)? "na" : value/5.0 - 500.0}},
         6 => {:name => :speed, :func => lambda{|value| (value == 0xffff)? "na" : value/1000000.0 * 60 * 60}}, #km/h
         7 => {:name => :power, :func => lambda{|value| (value == 0xffff)? "na" : value}}, 
         9 => {:name => :grade, :func => lambda{|value| (value == 0x7fff)? "na" : value/100.0}}, 
         33 => {:name => :calories, :func => lambda{|value| (value == -1)? "na" : value}}, 
         3 => {:name => :heart_rate, :func => lambda{|value| (value == 0xff)? "na" : value}}, 
         4 => {:name => :cadence, :func => lambda{|value| (value == 0xff)? "na" : value}}, 
         13 => {:name => :temperature, :func => lambda{|value| (value == 0x7f)? "na" : value}}, 
         30 => {:name => :left_right_balance, :func => lambda{|value| (value == 0xff)? "na": value}}, 
         43 => {:name => :left_torque_effectiveness, :func => lambda{|value| (value == 0xff)? "na": value/2.0}}, 
         44 => {:name => :right_torque_effectiveness, :func => lambda{|value| (value == 0xff)? "na": value/2.0}}, 
         45 => {:name => :left_pedal_smoothness, :func => lambda{|value| (value == 0xff)? "na": value/2.0}}, 
         46 => {:name => :right_pedal_smoothness, :func => lambda{|value| (value == 0xff)? "na": value/2.0}}, 
      }

解析プログラム的には

レコード定義ID(ダイナミックに変わる) -> global message id -> [[field1, size],[field2, size]…]

という感じで保持しておくと、次のデータレコードが読みやすくなるかと思います。

データレコード

流れとしては、

  • レコードの最初のバイトでレコード定義ID取得
  • レコード定義IDから、フィールド数とサイズを取得
  • 読み込み
  • 必要であればフィールド毎に人に読める形に変換(↑を参考に)

となります。

番号決め打ちでいろいろ定義されているのがちょっと面倒ですが、意外と綺麗な構造をしているので解析はしやすいと思います。

投げ銭していただける場合は、amazonで15円からできます。宛先はheisakuあっとcomichi.comで。

マイナスは入れられないの?

comments powered by Disqus