rubyfit 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.
|