サイコンのFitファイルを解析する
僕はレザインのサイクルコンピュータを使っているのですが、バックアップを取ると、.fitという拡張子でGPSのログが取られていることがわかります。
これは中身解析したくなりますよね?!
.fitファイルの仕様は、antのサイトで見つかりました。
Web Software Tools - THIS IS ANT
ここのFIT SDKから入手
.fitファイルの中身
この記事はサイクルコンピュータのファイル限定で書くので、フィットネス用の時計とかだと中に含まれる情報がちょっと違うと思います。
基本の解析方法は変わりませんので参考にはなると思います。
Fitファイルの構造は以下のようになります。
- ファイルヘッダ: 14バイト
- データヘッダ: 1バイト
- (データヘッダ & 0x40)!=0 の時は「レコード定義」が続く
レコード定義IDとして"データヘッダ^0x40"を使う - (データヘッダ & 0x40)==0 の時は「定義に基づいたデータ」が続く
レコード定義IDはこのデータヘッダの^0x40
- (データヘッダ & 0x40)!=0 の時は「レコード定義」が続く
- データ(レコード定義 または 定義に基づいたデータ)
以下、定義かデータが繰り返される
例えば、
フィールド | 内容 |
---|---|
ファイルヘッダ: 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],
]
グローバルメッセージ番号は、そのレコードが何を意味するか一意に決まる番号で、仕様書の中にあるエクセルファイルで定義されています。
例えば、僕のとある日のサイコンの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から、フィールド数とサイズを取得
- 読み込み
- 必要であればフィールド毎に人に読める形に変換(↑を参考に)
となります。
番号決め打ちでいろいろ定義されているのがちょっと面倒ですが、意外と綺麗な構造をしているので解析はしやすいと思います。