rubyfit 0.0.6 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ext/rubyfit/rubyfit.c +12 -0
- data/lib/rubyfit.rb +3 -0
- data/lib/rubyfit/helpers.rb +107 -0
- data/lib/rubyfit/message_writer.rb +176 -0
- data/lib/rubyfit/type.rb +155 -0
- data/lib/rubyfit/version.rb +1 -1
- data/lib/rubyfit/writer.rb +118 -0
- metadata +64 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 250aefed2638033617a97b1789e967c90a26940a
|
4
|
+
data.tar.gz: 46b607c278b112a5dffa6519d54f0bb2c585b0fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7fbc3064d7e5f8af5947228731c0c2f3213ea196cf4d9f0b2045968216bd41864e04d17c8a646694c5e3ac3541b73fe95861b2574ffa8d84453b9765263e9ca
|
7
|
+
data.tar.gz: 848eaca18d3bb03a830405671fd1c43e51020bfd51f4a6a66378c225b31e8b29f9d13bf82919c9473bf6fc77bbe03f76cecd443935b19d3ab337ab89b75ec307
|
data/ext/rubyfit/rubyfit.c
CHANGED
@@ -18,6 +18,7 @@
|
|
18
18
|
#include "math.h"
|
19
19
|
|
20
20
|
#include "fit_convert.h"
|
21
|
+
#include "fit_crc.h"
|
21
22
|
|
22
23
|
VALUE mRubyFit;
|
23
24
|
VALUE cFitParser;
|
@@ -540,6 +541,13 @@ static VALUE parse(VALUE self, VALUE original_str) {
|
|
540
541
|
return Qnil;
|
541
542
|
}
|
542
543
|
|
544
|
+
static VALUE update_crc(VALUE self, VALUE r_crc, VALUE r_data) {
|
545
|
+
FIT_UINT16 crc = NUM2USHORT(r_crc);
|
546
|
+
const char* data = StringValuePtr(r_data);
|
547
|
+
const FIT_UINT16 byte_count = RSTRING_LEN(r_data);
|
548
|
+
return UINT2NUM(FitCRC_Update16(crc, data, byte_count));
|
549
|
+
}
|
550
|
+
|
543
551
|
void Init_rubyfit() {
|
544
552
|
mRubyFit = rb_define_module("RubyFit");
|
545
553
|
cFitParser = rb_define_class_under(mRubyFit, "FitParser", rb_cObject);
|
@@ -551,4 +559,8 @@ void Init_rubyfit() {
|
|
551
559
|
//attributes
|
552
560
|
HANDLER_ATTR = rb_intern("@handler");
|
553
561
|
rb_define_attr(cFitParser, "handler", 1, 1);
|
562
|
+
|
563
|
+
// CRC helper
|
564
|
+
VALUE mCRC = rb_define_module_under(mRubyFit, "CRC");
|
565
|
+
rb_define_singleton_method(mCRC, "update_crc", update_crc, 2);
|
554
566
|
}
|
data/lib/rubyfit.rb
CHANGED
@@ -0,0 +1,107 @@
|
|
1
|
+
module RubyFit::Helpers
|
2
|
+
# Garmin timestamps start at 12:00:00 01-01-1989, 20 years after the unix epoch
|
3
|
+
GARMIN_TIME_OFFSET = 631065600
|
4
|
+
|
5
|
+
DEGREES_TO_SEMICIRCLES = 2**31 / 180.0
|
6
|
+
|
7
|
+
# Converts a fixnum or bignum into a byte array, optionally
|
8
|
+
# truncating or right-filling with 0 to match a certain size
|
9
|
+
def num2bytes(num, byte_count, big_endian = true)
|
10
|
+
raise ArgumentError.new("num must be an integer") unless num.is_a?(Integer)
|
11
|
+
orig_num = num
|
12
|
+
# Convert negative numbers to two's complement (1-byte alignment)
|
13
|
+
if num < 0
|
14
|
+
num = num.abs
|
15
|
+
|
16
|
+
if num > 2 ** (byte_count * 8 - 1)
|
17
|
+
STDERR.puts("RubyFit WARNING: Integer underflow for #{orig_num} (#{orig_num.bit_length + 1} bits) when fitting in #{byte_count} bytes (#{byte_count * 8} bits)")
|
18
|
+
end
|
19
|
+
|
20
|
+
num = 2 ** (byte_count * 8) - num
|
21
|
+
end
|
22
|
+
|
23
|
+
hex = num.to_s(16)
|
24
|
+
# pack('H*') assumes the high nybble is first, which reverses nybbles in
|
25
|
+
# the most significant byte if it's only one hex char (<= 0xF). Prevent
|
26
|
+
# this by prepending a zero if the hex string is an odd length
|
27
|
+
hex = "0" + hex if hex.length.odd?
|
28
|
+
result = [hex]
|
29
|
+
.pack('H*')
|
30
|
+
.unpack("C*")
|
31
|
+
|
32
|
+
if result.size > byte_count
|
33
|
+
STDERR.puts("RubyFit WARNING: Truncating #{orig_num} (#{orig_num.bit_length} bits) to fit in #{byte_count} bytes (#{byte_count * 8} bits)")
|
34
|
+
result = result.last(byte_count)
|
35
|
+
elsif result.size < byte_count
|
36
|
+
pad_bytes = [0] * (byte_count - result.size)
|
37
|
+
result.unshift(*pad_bytes)
|
38
|
+
end
|
39
|
+
|
40
|
+
result.reverse! unless big_endian
|
41
|
+
|
42
|
+
result
|
43
|
+
end
|
44
|
+
|
45
|
+
def bytes2num(bytes, byte_count, unsigned = true, big_endian = true)
|
46
|
+
directive = {
|
47
|
+
1 => "C",
|
48
|
+
2 => "S",
|
49
|
+
4 => "L",
|
50
|
+
8 => "Q"
|
51
|
+
}[byte_count]
|
52
|
+
raise "Unsupported byte count: #{byte_count}" unless directive
|
53
|
+
directive << (big_endian ? ">" : "<") if byte_count > 1
|
54
|
+
directive.downcase! unless unsigned
|
55
|
+
bytes.pack("C*").unpack(directive).first
|
56
|
+
end
|
57
|
+
|
58
|
+
# Converts an ASCII string into a byte array, truncating or right-filling
|
59
|
+
# with 0 to match byte_count
|
60
|
+
def str2bytes(str, byte_count)
|
61
|
+
str
|
62
|
+
.unpack("C#{byte_count - 1}") # Convert to n-1 bytes
|
63
|
+
.map{|v| v || 0} + [0] # Convert nils to 0 and add null terminator
|
64
|
+
end
|
65
|
+
|
66
|
+
# Converts a byte array to a string. Omits the last character of the byte
|
67
|
+
# array from the result if it is 0
|
68
|
+
def bytes2str(bytes)
|
69
|
+
bytes = bytes[0...-1] if bytes.last == 0
|
70
|
+
bytes.pack("C*")
|
71
|
+
end
|
72
|
+
|
73
|
+
# Generates strings of hex bytes (for debugging)
|
74
|
+
def bytes2hex(bytes)
|
75
|
+
bytes
|
76
|
+
.map{|b| "0x#{b.to_s(16).ljust(2, "0")}"}
|
77
|
+
.each_slice(8)
|
78
|
+
.map{ |s| s.join(", ") }
|
79
|
+
end
|
80
|
+
|
81
|
+
def unix2fit_timestamp(timestamp)
|
82
|
+
timestamp - GARMIN_TIME_OFFSET
|
83
|
+
end
|
84
|
+
|
85
|
+
def fit2unix_timestamp(timestamp)
|
86
|
+
timestamp + GARMIN_TIME_OFFSET
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def deg2semicircles(degrees)
|
91
|
+
(degrees * DEGREES_TO_SEMICIRCLES).truncate
|
92
|
+
end
|
93
|
+
|
94
|
+
def semicircles2deg(degrees)
|
95
|
+
result = degrees / DEGREES_TO_SEMICIRCLES
|
96
|
+
result -= 360.0 if result > 180.0
|
97
|
+
result += 360.0 if result < -180.0
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
101
|
+
def make_message_header(opts = {})
|
102
|
+
result = 0
|
103
|
+
result |= (1 << 6) if opts[:definition]
|
104
|
+
result |= (opts[:local_number] || 0) & 0xF
|
105
|
+
result
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require "rubyfit/type"
|
2
|
+
require "rubyfit/helpers"
|
3
|
+
|
4
|
+
class RubyFit::MessageWriter
|
5
|
+
extend RubyFit::Helpers
|
6
|
+
|
7
|
+
FIT_PROTOCOL_VERSION = 0x10 # major 1, minor 0
|
8
|
+
FIT_PROFILE_VERSION = 1 * 100 + 52 # major 1, minor 52
|
9
|
+
|
10
|
+
COURSE_POINT_TYPE = {
|
11
|
+
invalid: 255,
|
12
|
+
generic: 0,
|
13
|
+
summit: 1,
|
14
|
+
valley: 2,
|
15
|
+
water: 3,
|
16
|
+
food: 4,
|
17
|
+
danger: 5,
|
18
|
+
left: 6,
|
19
|
+
right: 7,
|
20
|
+
straight: 8,
|
21
|
+
first_aid: 9,
|
22
|
+
fourth_category: 10,
|
23
|
+
third_category: 11,
|
24
|
+
second_category: 12,
|
25
|
+
first_category: 13,
|
26
|
+
hors_category: 14,
|
27
|
+
sprint: 15,
|
28
|
+
left_fork: 16,
|
29
|
+
right_fork: 17,
|
30
|
+
middle_fork: 18,
|
31
|
+
slight_left: 19,
|
32
|
+
sharp_left: 20,
|
33
|
+
slight_right: 21,
|
34
|
+
sharp_right: 22,
|
35
|
+
u_turn: 23,
|
36
|
+
segment_start: 24,
|
37
|
+
segment_end: 25
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
MESSAGE_DEFINITIONS = {
|
41
|
+
file_id: {
|
42
|
+
id: 0,
|
43
|
+
fields: {
|
44
|
+
serial_number: { id: 3, type: RubyFit::Type.uint32z, required: true },
|
45
|
+
time_created: { id: 4, type: RubyFit::Type.timestamp, required: true },
|
46
|
+
manufacturer: { id: 1, type: RubyFit::Type.uint16 }, # See FIT_MANUFACTURER_*
|
47
|
+
product: { id: 2, type: RubyFit::Type.uint16 },
|
48
|
+
type: { id: 0, type: RubyFit::Type.enum, required: true }, # See FIT_FILE_*
|
49
|
+
}
|
50
|
+
},
|
51
|
+
course: {
|
52
|
+
id: 31,
|
53
|
+
fields: {
|
54
|
+
name: { id: 5, type: RubyFit::Type.string(16), required: true },
|
55
|
+
}
|
56
|
+
},
|
57
|
+
lap: {
|
58
|
+
id: 19,
|
59
|
+
fields: {
|
60
|
+
timestamp: { id: 253, type: RubyFit::Type.timestamp, required: true},
|
61
|
+
start_time: { id: 2, type: RubyFit::Type.timestamp, required: true},
|
62
|
+
start_y: { id: 3, type: RubyFit::Type.semicircles },
|
63
|
+
start_x: { id: 4, type: RubyFit::Type.semicircles },
|
64
|
+
end_y: { id: 5, type: RubyFit::Type.semicircles },
|
65
|
+
end_x: { id: 6, type: RubyFit::Type.semicircles },
|
66
|
+
total_distance: { id: 9, type: RubyFit::Type.centimeters },
|
67
|
+
},
|
68
|
+
},
|
69
|
+
course_point: {
|
70
|
+
id: 32,
|
71
|
+
fields: {
|
72
|
+
timestamp: { id: 1, type: RubyFit::Type.timestamp, required: true },
|
73
|
+
y: { id: 2, type: RubyFit::Type.semicircles, required: true },
|
74
|
+
x: { id: 3, type: RubyFit::Type.semicircles, required: true },
|
75
|
+
distance: { id: 4, type: RubyFit::Type.centimeters },
|
76
|
+
name: { id: 6, type: RubyFit::Type.string(16) },
|
77
|
+
message_index: { id: 254, type: RubyFit::Type.uint16 },
|
78
|
+
type: { id: 5, type: RubyFit::Type.enum, values: COURSE_POINT_TYPE, required: true }
|
79
|
+
},
|
80
|
+
},
|
81
|
+
record: {
|
82
|
+
id: 20,
|
83
|
+
fields: {
|
84
|
+
timestamp: { id: 253, type: RubyFit::Type.timestamp, required: true },
|
85
|
+
y: { id: 0, type: RubyFit::Type.semicircles, required: true },
|
86
|
+
x: { id: 1, type: RubyFit::Type.semicircles, required: true },
|
87
|
+
distance: { id: 5, type: RubyFit::Type.centimeters },
|
88
|
+
elevation: { id: 2, type: RubyFit::Type.altitude },
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
def self.definition_message(type, local_num)
|
94
|
+
pack_bytes do |bytes|
|
95
|
+
message_data = MESSAGE_DEFINITIONS[type]
|
96
|
+
bytes << header_byte(local_num, true)
|
97
|
+
bytes << 0x00 # Reserved uint8
|
98
|
+
bytes << 0x01 # Big endian
|
99
|
+
bytes.push(*num2bytes(message_data[:id], 2)) # Global message ID
|
100
|
+
bytes << message_data[:fields].size # Field count
|
101
|
+
|
102
|
+
message_data[:fields].each do |field, info|
|
103
|
+
type = info[:type]
|
104
|
+
bytes << info[:id]
|
105
|
+
bytes << type.byte_count
|
106
|
+
bytes << type.fit_id
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.data_message(type, local_num, values)
|
112
|
+
pack_bytes do |bytes|
|
113
|
+
message_data = MESSAGE_DEFINITIONS[type]
|
114
|
+
bytes << header_byte(local_num, false)
|
115
|
+
message_data[:fields].each do |field, info|
|
116
|
+
field_type = info[:type]
|
117
|
+
value = values[field]
|
118
|
+
if info[:required] && value.nil?
|
119
|
+
raise ArgumentError.new("Missing required field '#{field}' in #{type} data message values")
|
120
|
+
end
|
121
|
+
|
122
|
+
if info[:values]
|
123
|
+
value = info[:values][value]
|
124
|
+
if value.nil?
|
125
|
+
raise ArgumentError.new("Invalid value for '#{field}' in #{type} data message values")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
value_bytes = value ? field_type.val2bytes(value) : field_type.default_bytes
|
130
|
+
bytes.push(*value_bytes)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.definition_message_size(type)
|
136
|
+
message_data = MESSAGE_DEFINITIONS[type]
|
137
|
+
raise ArgumentError.new("Unknown message type '#{type}'") unless message_data
|
138
|
+
6 + message_data[:fields].count * 3
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.data_message_size(type)
|
142
|
+
message_data = MESSAGE_DEFINITIONS[type]
|
143
|
+
raise ArgumentError.new("Unknown message type '#{type}'") unless message_data
|
144
|
+
1 + message_data[:fields].values.map{|info| info[:type].byte_count}.reduce(&:+)
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.file_header(data_byte_count = 0)
|
148
|
+
pack_bytes do |bytes|
|
149
|
+
bytes << 14 # Header size
|
150
|
+
bytes << FIT_PROTOCOL_VERSION # Protocol version
|
151
|
+
bytes.push(*num2bytes(FIT_PROFILE_VERSION, 2).reverse) # Profile version (little endian)
|
152
|
+
bytes.push(*num2bytes(data_byte_count, 4).reverse) # Data size (little endian)
|
153
|
+
bytes.push(*str2bytes(".FIT", 5).take(4)) # Data Type ASCII, no terminator
|
154
|
+
crc = 0 #RubyFit::CRC.update_crc(0, bytes2str(bytes))
|
155
|
+
bytes.push(*num2bytes(crc, 2).reverse) # Header CRC (little endian)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.crc(crc_value)
|
160
|
+
pack_bytes do |bytes|
|
161
|
+
bytes.push(*num2bytes(crc_value, 2, false)) # Little endian
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Internal
|
166
|
+
|
167
|
+
def self.header_byte(local_number, definition)
|
168
|
+
local_number & 0xF | (definition ? 0x40 : 0x00)
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.pack_bytes
|
172
|
+
bytes = []
|
173
|
+
yield bytes
|
174
|
+
bytes.pack("C*")
|
175
|
+
end
|
176
|
+
end
|
data/lib/rubyfit/type.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
require "rubyfit/helpers"
|
2
|
+
|
3
|
+
class RubyFit::Type
|
4
|
+
attr_reader *%i(fit_id byte_count default_bytes)
|
5
|
+
|
6
|
+
def initialize(opts = {})
|
7
|
+
@val2bytes = opts[:val2bytes]
|
8
|
+
@bytes2val = opts[:bytes2val]
|
9
|
+
@rb2fit = opts[:rb2fit]
|
10
|
+
@fit2rb = opts[:fit2rb]
|
11
|
+
@default_bytes = opts[:default_bytes]
|
12
|
+
@byte_count = opts[:byte_count]
|
13
|
+
@fit_id = opts[:fit_id]
|
14
|
+
end
|
15
|
+
|
16
|
+
def val2bytes(val)
|
17
|
+
result = val
|
18
|
+
result = @rb2fit.call(result, self) if @rb2fit
|
19
|
+
result = @val2bytes.call(result, self)
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def bytes2val(bytes)
|
24
|
+
result = bytes
|
25
|
+
result = @bytes2val.call(result, self)
|
26
|
+
result = @fit2rb.call(result, self) if @fit2rb
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
include RubyFit::Helpers
|
32
|
+
|
33
|
+
def integer(opts = {})
|
34
|
+
unsigned = opts.delete(:unsigned)
|
35
|
+
default = opts[:default]
|
36
|
+
|
37
|
+
# Default (invalid) value for integers is the maximum positive value
|
38
|
+
# given the bit length and whether the data is signed/unsigned
|
39
|
+
unless default
|
40
|
+
bit_count = opts[:byte_count] * 8
|
41
|
+
bit_count -= 1 unless unsigned
|
42
|
+
default = 2**bit_count - 1
|
43
|
+
end
|
44
|
+
|
45
|
+
new({
|
46
|
+
default_bytes: num2bytes(default, opts[:byte_count]),
|
47
|
+
val2bytes: ->(val, type) { num2bytes(val, type.byte_count) },
|
48
|
+
bytes2val: ->(bytes, type) { bytes2num(bytes, type.byte_count, unsigned) },
|
49
|
+
}.merge(opts))
|
50
|
+
end
|
51
|
+
|
52
|
+
# Base Types #
|
53
|
+
|
54
|
+
def enum(opts = {})
|
55
|
+
uint8(fit_id: 0x00)
|
56
|
+
end
|
57
|
+
|
58
|
+
def string(byte_count, opts = {})
|
59
|
+
new({
|
60
|
+
fit_id: 0x07,
|
61
|
+
byte_count: byte_count,
|
62
|
+
default_bytes: [0x00] * byte_count,
|
63
|
+
val2bytes: ->(val, type) { str2bytes(val, type.byte_count) },
|
64
|
+
bytes2val: ->(bytes, type) { bytes2str(bytes) },
|
65
|
+
}.merge(opts))
|
66
|
+
end
|
67
|
+
|
68
|
+
def byte(byte_count, opts = {})
|
69
|
+
new({
|
70
|
+
fit_id: 0x0D,
|
71
|
+
default_bytes: [0xFF] * length,
|
72
|
+
val2bytes: ->(val) { val },
|
73
|
+
bytes2val: ->(bytes) { bytes },
|
74
|
+
}.merge(opts))
|
75
|
+
end
|
76
|
+
|
77
|
+
def sint8(opts = {})
|
78
|
+
integer({unsigned: false, byte_count: 1, fit_id: 0x01}.merge(opts))
|
79
|
+
end
|
80
|
+
|
81
|
+
def uint8(opts = {})
|
82
|
+
integer({unsigned: true, byte_count: 1, fit_id: 0x02}.merge(opts))
|
83
|
+
end
|
84
|
+
|
85
|
+
def sint16(opts = {})
|
86
|
+
integer({unsigned: false, byte_count: 2, fit_id: 0x83}.merge(opts))
|
87
|
+
end
|
88
|
+
|
89
|
+
def uint16(opts = {})
|
90
|
+
integer({unsigned: true, byte_count: 2, fit_id: 0x84}.merge(opts))
|
91
|
+
end
|
92
|
+
|
93
|
+
def sint32(opts = {})
|
94
|
+
integer({unsigned: false, byte_count: 4, fit_id: 0x85}.merge(opts))
|
95
|
+
end
|
96
|
+
|
97
|
+
def uint32(opts = {})
|
98
|
+
integer({unsigned: true, byte_count: 4, fit_id: 0x86}.merge(opts))
|
99
|
+
end
|
100
|
+
|
101
|
+
def sint64(opts = {})
|
102
|
+
integer({unsigned: false, byte_count: 8, fit_id: 0x8E}.merge(opts))
|
103
|
+
end
|
104
|
+
|
105
|
+
def uint64(opts = {})
|
106
|
+
integer({unsigned: true, byte_count: 8, fit_id: 0x8F}.merge(opts))
|
107
|
+
end
|
108
|
+
|
109
|
+
def uint8z(opts = {})
|
110
|
+
integer({unsigned: true, default: 0, byte_count: 1, fit_id: 0x0A}.merge(opts))
|
111
|
+
end
|
112
|
+
|
113
|
+
def uint16z(opts = {})
|
114
|
+
integer({unsigned: true, default: 0, byte_count: 2, fit_id: 0x8B}.merge(opts))
|
115
|
+
end
|
116
|
+
|
117
|
+
def uint32z(opts = {})
|
118
|
+
integer({unsigned: true, default: 0, byte_count: 4, fit_id: 0x8C}.merge(opts))
|
119
|
+
end
|
120
|
+
|
121
|
+
def uint64z(opts = {})
|
122
|
+
integer({unsigned: true, default: 0, byte_count: 8, fit_id: 0x90}.merge(opts))
|
123
|
+
end
|
124
|
+
|
125
|
+
# Derived types
|
126
|
+
|
127
|
+
def timestamp
|
128
|
+
uint32({
|
129
|
+
rb2fit: ->(val, type) { unix2fit_timestamp(val) },
|
130
|
+
fit2rb: ->(val, type) { fit2unix_timestamp(val) }
|
131
|
+
})
|
132
|
+
end
|
133
|
+
|
134
|
+
def semicircles
|
135
|
+
sint32({
|
136
|
+
rb2fit: ->(val, type) { deg2semicircles(val) },
|
137
|
+
fit2rb: ->(val, type) { semicircles2deg(val) }
|
138
|
+
})
|
139
|
+
end
|
140
|
+
|
141
|
+
def centimeters
|
142
|
+
uint32({
|
143
|
+
rb2fit: ->(val, type) { (val * 100).truncate },
|
144
|
+
fit2rb: ->(val, type) { val / 100.0 }
|
145
|
+
})
|
146
|
+
end
|
147
|
+
|
148
|
+
def altitude
|
149
|
+
uint16({
|
150
|
+
rb2fit: ->(val, type) { (val + 500).truncate },
|
151
|
+
fit2rb: ->(val, type) { val - 500 }
|
152
|
+
})
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/rubyfit/version.rb
CHANGED
@@ -0,0 +1,118 @@
|
|
1
|
+
require "rubyfit/message_writer"
|
2
|
+
|
3
|
+
class RubyFit::Writer
|
4
|
+
def write(stream, opts = {})
|
5
|
+
raise "Can't start write mode from #{@state}" if @state
|
6
|
+
@state = :write
|
7
|
+
@local_nums = []
|
8
|
+
|
9
|
+
@stream = stream
|
10
|
+
|
11
|
+
%i(start_time end_time course_point_count track_point_count name
|
12
|
+
total_distance time_created start_x start_y end_x end_y).each do |key|
|
13
|
+
raise ArgumentError.new("Missing required option #{key}") unless opts[key]
|
14
|
+
end
|
15
|
+
|
16
|
+
start_time = opts[:start_time].to_i
|
17
|
+
end_time = opts[:end_time].to_i
|
18
|
+
|
19
|
+
@data_crc = 0
|
20
|
+
|
21
|
+
# Calculate data size to put in header
|
22
|
+
definition_sizes = %i(file_id course lap course_point record)
|
23
|
+
.map{|type| RubyFit::MessageWriter.definition_message_size(type) }
|
24
|
+
.reduce(&:+)
|
25
|
+
|
26
|
+
data_sizes = {
|
27
|
+
file_id: 1,
|
28
|
+
course: 1,
|
29
|
+
lap: 1,
|
30
|
+
course_point: opts[:course_point_count],
|
31
|
+
record: opts[:track_point_count]
|
32
|
+
}
|
33
|
+
.map{|type, count| RubyFit::MessageWriter.data_message_size(type) * count }
|
34
|
+
.reduce(&:+)
|
35
|
+
|
36
|
+
data_size = definition_sizes + data_sizes
|
37
|
+
write_data(RubyFit::MessageWriter.file_header(data_size))
|
38
|
+
|
39
|
+
write_definition_message(:file_id)
|
40
|
+
write_data_message(:file_id, {
|
41
|
+
time_created: opts[:time_created],
|
42
|
+
type: 6, # Course file
|
43
|
+
manufacturer: 1, # Garmin
|
44
|
+
product: 0,
|
45
|
+
serial_number: 0,
|
46
|
+
})
|
47
|
+
|
48
|
+
write_definition_message(:course)
|
49
|
+
write_data_message(:course, { name: opts[:name] })
|
50
|
+
|
51
|
+
write_definition_message(:lap)
|
52
|
+
write_data_message(:lap, {
|
53
|
+
start_time: start_time,
|
54
|
+
timestamp: end_time,
|
55
|
+
start_x: opts[:start_x],
|
56
|
+
start_y: opts[:start_y],
|
57
|
+
end_x: opts[:end_x],
|
58
|
+
end_y: opts[:end_y],
|
59
|
+
total_distance: opts[:total_distance]
|
60
|
+
})
|
61
|
+
|
62
|
+
yield
|
63
|
+
|
64
|
+
write_data(RubyFit::MessageWriter.crc(@data_crc))
|
65
|
+
@state = nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def course_points
|
69
|
+
raise "Can only start course points mode inside 'write' block" if @state != :write
|
70
|
+
@state = :course_points
|
71
|
+
write_definition_message(:course_point)
|
72
|
+
yield
|
73
|
+
@state = :write
|
74
|
+
end
|
75
|
+
|
76
|
+
def track_points
|
77
|
+
raise "Can only write track points inside 'write' block" if @state != :write
|
78
|
+
@state = :track_points
|
79
|
+
write_definition_message(:record)
|
80
|
+
yield
|
81
|
+
@state = :write
|
82
|
+
end
|
83
|
+
|
84
|
+
def course_point(values)
|
85
|
+
raise "Can only write course points inside 'course_points' block" if @state != :course_points
|
86
|
+
write_data_message(:course_point, values)
|
87
|
+
end
|
88
|
+
|
89
|
+
def track_point(values)
|
90
|
+
raise "Can only write track points inside 'track_points' block" if @state != :track_points
|
91
|
+
write_data_message(:record, values)
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
def write_definition_message(type)
|
97
|
+
write_data(RubyFit::MessageWriter.definition_message(type, local_num(type)))
|
98
|
+
end
|
99
|
+
|
100
|
+
def write_data_message(type, values)
|
101
|
+
write_data(RubyFit::MessageWriter.data_message(type, local_num(type), values))
|
102
|
+
end
|
103
|
+
|
104
|
+
def write_data(data)
|
105
|
+
@stream.write(data)
|
106
|
+
prev = @data_crc
|
107
|
+
@data_crc = RubyFit::CRC.update_crc(@data_crc, data)
|
108
|
+
end
|
109
|
+
|
110
|
+
def local_num(type)
|
111
|
+
result = @local_nums.index(type)
|
112
|
+
unless result
|
113
|
+
result = @local_nums.size
|
114
|
+
@local_nums << type
|
115
|
+
end
|
116
|
+
result
|
117
|
+
end
|
118
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,71 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubyfit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cullen King
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
11
|
+
date: 2018-02-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.13.0
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.13.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.9.1
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.9.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake-compiler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.8.3
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.8.3
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: awesome_print
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
13
69
|
description: FIT files are binary, and as a result, are a pain to parse. This is
|
14
70
|
a wrapper around the FIT SDK, which makes creating a stream based parser simple.
|
15
71
|
email:
|
@@ -34,7 +90,11 @@ files:
|
|
34
90
|
- ext/rubyfit/fit_ram.h
|
35
91
|
- ext/rubyfit/rubyfit.c
|
36
92
|
- lib/rubyfit.rb
|
93
|
+
- lib/rubyfit/helpers.rb
|
94
|
+
- lib/rubyfit/message_writer.rb
|
95
|
+
- lib/rubyfit/type.rb
|
37
96
|
- lib/rubyfit/version.rb
|
97
|
+
- lib/rubyfit/writer.rb
|
38
98
|
homepage: http://cullenking.com
|
39
99
|
licenses: []
|
40
100
|
metadata: {}
|
@@ -55,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
115
|
version: '0'
|
56
116
|
requirements: []
|
57
117
|
rubyforge_project: rubyfit
|
58
|
-
rubygems_version: 2.
|
118
|
+
rubygems_version: 2.5.2.1
|
59
119
|
signing_key:
|
60
120
|
specification_version: 4
|
61
121
|
summary: A stream based parser for FIT files.
|