px4_log_reader 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ module Px4LogReader
2
+
3
+ class MessageDescriptorCache
4
+
5
+ attr_reader :cache_filename
6
+
7
+ def initialize( filename )
8
+ @cache_filename = filename
9
+ end
10
+
11
+ def exist?
12
+ return File.exist?( @cache_filename )
13
+ end
14
+
15
+ def read_descriptors
16
+
17
+ message_descriptors = {}
18
+
19
+ if File.exist?( cache_filename )
20
+ File.open( cache_filename, 'r' ) do |input|
21
+ begin
22
+ while ( ( data = input.read(4) ) && ( data.length == 4 ) ) do
23
+ descriptor_size = data.unpack('L').first
24
+ descriptor = Marshal.load( input.read( descriptor_size ) )
25
+
26
+ message_descriptors[ descriptor.type ] = descriptor
27
+ end
28
+ rescue EOFError => error
29
+ puts "Parsed #{@message_descriptions.size} cached message descriptions"
30
+ rescue StandardError => error
31
+ puts "#{error.class}: #{error.message}"
32
+ puts error.backtrace.join("\n")
33
+ end
34
+ end
35
+ else
36
+ puts "Cache file '#{cache_filename}' not found"
37
+ end
38
+
39
+ return message_descriptors
40
+ end
41
+
42
+ def write_descriptors( message_descriptors )
43
+ if !@cache_filename.empty? && File.exist?( File.dirname( @cache_filename ) )
44
+ File.open( @cache_filename, 'w+' ) do |output|
45
+ message_descriptors.each do |message_type,descriptor|
46
+ descriptor_data = Marshal.dump( descriptor )
47
+ output.write( [ descriptor_data.size ].pack('L') )
48
+ output.write( descriptor_data )
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,160 @@
1
+ module Px4LogReader
2
+
3
+ def self.open_common( file, options, &block )
4
+
5
+ reader = Reader.new( file, options )
6
+
7
+ yield reader if block_given?
8
+
9
+ return reader
10
+ end
11
+
12
+ def self.open( filename, options = {}, &block )
13
+
14
+ reader = nil
15
+
16
+ if File.exist?( filename )
17
+ reader = self.open_common( File.open( filename, 'r' ), options, &block )
18
+ end
19
+
20
+ return reader
21
+
22
+ end
23
+
24
+ def self.open!( filename, options = {}, &block )
25
+ reader = nil
26
+
27
+ if File.exist?( filename )
28
+ reader = self.open_common( File.open( filename, 'r' ), options, &block )
29
+ else
30
+ raise FileNotFoundError.new( filename )
31
+ end
32
+
33
+ return reader
34
+ end
35
+
36
+ class Context
37
+ attr_reader :messages
38
+ def initialize
39
+ messages = {}
40
+ end
41
+ def find_by_name( name )
42
+ named_message = nil
43
+ @messages.values.each do |message|
44
+ if message.descriptor.name == name
45
+ named_message = message
46
+ end
47
+ end
48
+ return named_message
49
+ end
50
+ def find_by_type( type )
51
+ return @messages[ type ]
52
+ end
53
+ def set( message )
54
+ @messages[ message.descriptor.type ] = message.dup
55
+ end
56
+ end
57
+
58
+ class Reader
59
+
60
+ def initialize( file, options )
61
+
62
+ opts = {
63
+ cache_filename: '',
64
+ buffer_size_kb: 10 * 1024
65
+ }.merge( options )
66
+
67
+ @message_descriptors = {}
68
+ @buffers = LogBufferArray.new
69
+ @descriptor_cache = nil
70
+ @context = Context.new
71
+
72
+ @log_file = file
73
+ # @buffers.set_file( @log_file, load_buffers: true )
74
+
75
+ @descriptor_cache = MessageDescriptorCache.new( opts[:cache_filename] )
76
+ end
77
+
78
+ def descriptors
79
+ if @log_file && @message_descriptors.empty?
80
+ if @descriptor_cache && @descriptor_cache.exist?
81
+ @message_descriptors = @descriptor_cache.read_descriptors
82
+ else
83
+ @message_descriptors = LogFile::read_descriptors( @log_file, @descriptor_cache )
84
+ end
85
+
86
+ @message_descriptors[ FORMAT_MESSAGE.type ] = FORMAT_MESSAGE
87
+ end
88
+
89
+ return @message_descriptors
90
+ end
91
+
92
+ def each_message( options = {}, &block )
93
+
94
+ opts ={
95
+ with: [], # white list - empty means all minus those in without list
96
+ without: ['FMT'] # black list - includes types or names
97
+ }.merge( options || {} )
98
+
99
+ opts[:with].map! do |val|
100
+ if val.class == String
101
+ descriptor = descriptors.values.find { |desc| desc.name == val }
102
+ val = descriptor.type
103
+ end
104
+ end
105
+
106
+ opts[:without].map! do |val|
107
+ if val.class == String
108
+ descriptor = descriptors.values.find { |desc| desc.name == val }
109
+
110
+ if descriptor
111
+ val = descriptor.type
112
+ else
113
+ raise "Failed to find descriptor with name '#{val}'"
114
+ end
115
+ end
116
+ end
117
+
118
+ if block_given?
119
+
120
+ loop do
121
+
122
+ message = LogFile::read_message( @log_file, @message_descriptors )
123
+ break if message.nil?
124
+
125
+ # Added message to the set of latest messages.
126
+ @context.set( message )
127
+
128
+ if opts[:with].empty?
129
+ if !opts[:without].include?( message.descriptor.name )
130
+ yield message, @context
131
+ end
132
+ else
133
+ if opts[:with].include?( message.descriptor.type )
134
+ yield message, @context
135
+ end
136
+ end
137
+
138
+ end
139
+
140
+ else
141
+ raise BlockRequiredError.new
142
+ end
143
+
144
+ end
145
+
146
+ # def rewind
147
+ # if @log_file
148
+
149
+ # @log_file.rewind
150
+ # @buffers.load_buffers
151
+
152
+ # end
153
+ # end
154
+
155
+ # def seek( offset )
156
+ # end
157
+
158
+ end
159
+
160
+ end
@@ -0,0 +1,3 @@
1
+ module Px4LogReader
2
+ VERSION = '0.0.5'
3
+ end
@@ -0,0 +1,8 @@
1
+ require 'px4_log_reader/version'
2
+ require 'px4_log_reader/invalid_descriptor_error'
3
+ require 'px4_log_reader/log_message'
4
+ require 'px4_log_reader/message_descriptor'
5
+ require 'px4_log_reader/log_buffer'
6
+ require 'px4_log_reader/log_file'
7
+ require 'px4_log_reader/message_descriptor_cache'
8
+ require 'px4_log_reader/reader'
@@ -0,0 +1,228 @@
1
+
2
+
3
+
4
+
5
+ # class Px4LogReader
6
+
7
+ # include PX4
8
+
9
+ # HEADER_MARKER = [0xA3,0x95]#.pack('CC').freeze
10
+ # HEADER_LENGTH = 3
11
+
12
+ # FORMAT_MESSAGE = Px4LogMessageDescription.new({
13
+ # name: 'FMT',
14
+ # type: 0x80,
15
+ # length: 89,
16
+ # format: 'BBnZ',
17
+ # fields: ["Type", "Length", "Name", "Format", "Labels"] }).freeze
18
+
19
+ # attr_reader :message_descriptions
20
+ # attr_reader :messages
21
+ # attr_reader :px4_log_format
22
+
23
+ # def initialize
24
+ # @message_descriptions = {}
25
+ # @messages = []
26
+ # @px4_log_format = false
27
+ # end
28
+
29
+ # def parse_log( filename, cache_filename=nil )
30
+ # @message_descriptions = {}
31
+ # @messages = []
32
+
33
+ # @file_size = File.size?(filename)
34
+
35
+ # if cache_filename && File.exist?( cache_filename )
36
+ # if File.exist?( cache_filename )
37
+ # File.open( cache_filename, 'r' ) do |io|
38
+ # begin
39
+ # loop do
40
+ # description_size = io.read_nonblock(4).unpack('L').first
41
+ # description = Marshal.load( io.read(description_size) )
42
+
43
+ # @message_descriptions[ description.type ] = description
44
+ # end
45
+ # rescue EOFError => error
46
+ # puts "Parsed #{@message_descriptions.size} cached message descriptions"
47
+ # # @message_descriptions.each do |message_type,description|
48
+ # # puts description
49
+ # # end
50
+ # rescue StandardError => error
51
+ # puts "#{error.class}: #{error.message}"
52
+ # puts error.backtrace.join("\n")
53
+ # end
54
+ # end
55
+ # else
56
+ # puts "Cache file '#{cache_filename}' not found"
57
+ # end
58
+ # else
59
+ # File.open( filename, 'r' ) do |io|
60
+ # read_formats( io, cache_filename )
61
+ # end
62
+ # end
63
+
64
+ # if @message_descriptions.size > 0
65
+ # File.open( filename, 'r' ) do |io|
66
+ # begin
67
+ # loop do
68
+ # message = read_message( io )
69
+
70
+ # if message.nil?
71
+ # puts "Failed to read message"
72
+ # break
73
+ # elsif message.description.name != "FMT"
74
+ # @messages << message
75
+ # end
76
+
77
+ # $stdout.printf "\rReading messages %d/%d", io.pos, @file_size
78
+ # end
79
+ # rescue StandardError => error
80
+ # puts "#{error.class}: #{error.message}"
81
+ # puts error.backtrace.join("\n")
82
+ # end
83
+ # end
84
+ # else
85
+ # raise "No message descriptions found"
86
+ # end
87
+ # puts
88
+ # end
89
+
90
+ # def read_message_header( io )
91
+ # byte = nil
92
+
93
+ # begin
94
+
95
+ # data = io.read(2)
96
+
97
+ # if data && data.length == 2
98
+ # loop do
99
+
100
+ # data << io.read_nonblock(1)
101
+
102
+ # # puts "#{data.unpack('CCC')[0,2]} == #{HEADER_MARKER}"
103
+ # if data.unpack('CCC')[0,2] == HEADER_MARKER
104
+ # byte = data.unpack('CCC').last & 0xFF
105
+ # # puts "Found message header #{data.unpack('CCC')}"
106
+ # # puts "message_type = #{'%02X'%byte}"
107
+ # break
108
+ # else
109
+ # data = data[1..-1]
110
+ # end
111
+
112
+ # end
113
+ # end
114
+
115
+ # rescue EOFError => error
116
+ # # Nothing to do.
117
+ # rescue StandardError => error
118
+ # puts error.message
119
+ # puts error.backtrace.join("\n")
120
+ # end
121
+
122
+ # return byte
123
+ # end
124
+
125
+ # def read_message( io )
126
+
127
+ # message = nil
128
+ # while message.nil? do
129
+ # message_type = read_message_header( io )
130
+
131
+ # if message_type && (message_type != FORMAT_MESSAGE.type)
132
+ # message_description = @message_descriptions[ message_type ]
133
+
134
+ # if message_description
135
+ # message_data = io.read( message_description.length - HEADER_LENGTH )
136
+ # message = message_description.parse_message( message_data )
137
+ # else
138
+ # puts "ERROR: Failed to get description for message of type '#{'0x%02X' % message_type}'"
139
+ # end
140
+ # elsif message_type.nil?
141
+ # break
142
+ # end
143
+
144
+ # # $stdout.printf "\rReading formats %d/%d", io.pos, @file_size
145
+ # end
146
+
147
+ # return message
148
+ # end
149
+
150
+ # def read_formats( io, cache_filename )
151
+ # loop do
152
+ # begin
153
+ # message_type = read_message_header( io )
154
+
155
+ # if message_type.nil?
156
+ # break
157
+ # elsif message_type == FORMAT_MESSAGE.type
158
+ # # puts "Found format message"
159
+ # message_description = Px4LogMessageDescription.new
160
+ # message_description.parse_from_io( io )
161
+
162
+ # unless @message_descriptions.keys.include? message_description.type
163
+ # @message_descriptions[message_description.type] = message_description
164
+ # end
165
+
166
+ # if message_description.name == "TIME"
167
+ # @px4_log_format = true
168
+ # end
169
+ # end
170
+
171
+ # $stdout.printf "\rReading formats %d/%d", io.pos, @file_size
172
+
173
+ # rescue StandardError => e
174
+ # puts "#{e.class}: #{e.message}"
175
+ # puts e.backtrace.join("\n")
176
+ # break
177
+ # end
178
+ # end
179
+ # $stdout.puts
180
+
181
+ # if cache_filename
182
+ # File.open( cache_filename, 'w+' ) do |io|
183
+ # @message_descriptions.each do |message_type,description|
184
+ # description_data = Marshal.dump( description )
185
+ # io.write( [ description_data.size ].pack('L') )
186
+ # io.write( description_data )
187
+ # end
188
+ # end
189
+ # end
190
+ # end
191
+
192
+ # end
193
+
194
+
195
+ # if ARGV.size == 2
196
+ # filename = ARGV[0]
197
+ # output_dir = ARGV[1]
198
+ # puts "Attempting to parse #{filename}"
199
+
200
+ # cache_filename = File.join( 'px4_csvs', "#{File.basename( filename, '.px4log' )}.px4log_description_cache" )
201
+
202
+ # log = Px4LogReader.new
203
+ # log.parse_log( filename, cache_filename )
204
+
205
+ # log.message_descriptions.each do |type,description|
206
+ # # puts description
207
+ # File.open(File.join(output_dir,"#{description.name.downcase}_log.csv"),"w+") do |io|
208
+ # io.puts description.to_csv_line
209
+ # end
210
+ # end
211
+
212
+ # last_timestamp = 0
213
+ # log.messages.each do |message|
214
+ # if message.description.name == "TIME"
215
+ # last_timestamp = message.get(0)
216
+ # end
217
+
218
+ # File.open(File.join(output_dir,"#{message.description.name.downcase}_log.csv"),"a+") do |io|
219
+ # io.puts message.to_csv_line(last_timestamp)
220
+ # end
221
+ # end
222
+
223
+ # else
224
+
225
+ # puts "Specify source and destination"
226
+
227
+ # end
228
+
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require_relative 'lib/px4_log_reader/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'px4_log_reader'
6
+ s.version = Px4LogReader::VERSION
7
+ s.date = Time.now.to_date.strftime('%Y-%m-%d')
8
+ s.summary = "PX4 flight log reader"
9
+ s.description = "Px4LogReader is a gem for parsing PX4-format flight logs generated by the Pixhawk."
10
+ s.authors = [ "Robert Glissmann" ]
11
+ s.email = 'Robert.Glissmann@gmail.com'
12
+ s.files = `git ls-files`.split("\n")
13
+ s.licenses = ['BSD']
14
+ s.homepage = 'https://github.com/rgmann/px4_log_reader'
15
+
16
+ s.add_development_dependency 'rspec', '~> 3.1'
17
+ end
data/test/test.rb ADDED
@@ -0,0 +1,33 @@
1
+ if ARGV.size == 2
2
+ filename = ARGV[0]
3
+ output_dir = ARGV[1]
4
+ puts "Attempting to parse #{filename}"
5
+
6
+ cache_filename = File.join( 'px4_csvs', "#{File.basename( filename, '.px4log' )}.px4log_description_cache" )
7
+
8
+ log = Px4LogReader.new
9
+ log.parse_log( filename, cache_filename )
10
+
11
+ log.message_descriptions.each do |type,description|
12
+ # puts description
13
+ File.open(File.join(output_dir,"#{description.name.downcase}_log.csv"),"w+") do |io|
14
+ io.puts description.to_csv_line
15
+ end
16
+ end
17
+
18
+ last_timestamp = 0
19
+ log.messages.each do |message|
20
+ if message.description.name == "TIME"
21
+ last_timestamp = message.get(0)
22
+ end
23
+
24
+ File.open(File.join(output_dir,"#{message.description.name.downcase}_log.csv"),"a+") do |io|
25
+ io.puts message.to_csv_line(last_timestamp)
26
+ end
27
+ end
28
+
29
+ else
30
+
31
+ puts "Specify source and destination"
32
+
33
+ end
Binary file
@@ -0,0 +1,173 @@
1
+ require 'minitest/autorun'
2
+ require 'px4_log_reader'
3
+
4
+ class TestLogBuffer < MiniTest::Test
5
+
6
+ def setup
7
+ end
8
+
9
+ def test_buffer_create
10
+
11
+ buffer_size = 256
12
+ buffer = Px4LogReader::LogBuffer.new( buffer_size )
13
+
14
+ assert_equal buffer_size, buffer.data.size
15
+ assert_equal Array.new( buffer_size, 0x00 ), buffer.data
16
+ assert_equal 0, buffer.read_position
17
+ assert_equal 0, buffer.write_position
18
+ assert_equal true, buffer.empty?
19
+
20
+ end
21
+
22
+ def test_buffer_read_write
23
+
24
+ buffer_size = 256
25
+ buffer = Px4LogReader::LogBuffer.new( buffer_size )
26
+
27
+ test_filename = './test_buffer_read_write.bin'
28
+ generate_test_file( test_filename, buffer_size )
29
+
30
+ test_file = File.open( test_filename,'rb+')
31
+
32
+ assert_equal true, buffer.empty?
33
+
34
+ buffer.write( test_file )
35
+ assert_equal 0, buffer.read_position
36
+ assert_equal 256, buffer.write_position
37
+ assert_equal false, buffer.empty?
38
+
39
+ test_read_1_size = 112
40
+ data = buffer.read( test_read_1_size )
41
+ assert_equal test_read_1_size, data.size
42
+ assert_equal test_read_1_size, buffer.read_position
43
+ assert_equal buffer_size, buffer.write_position
44
+ assert_equal false, buffer.empty?
45
+
46
+ test_read_2_size = 256
47
+ data = buffer.read( test_read_2_size )
48
+ assert_equal (buffer_size - test_read_1_size), data.size
49
+ assert_equal buffer_size, buffer.read_position
50
+ assert_equal buffer_size, buffer.write_position
51
+ assert_equal true, buffer.empty?
52
+
53
+ test_file.close
54
+
55
+ FileUtils.rm test_filename
56
+
57
+ end
58
+
59
+ def test_buffer_array
60
+
61
+ buffer_array = Px4LogReader::LogBufferArray.new
62
+
63
+ file_size = 256
64
+ test_filename = './test_buffer_array.bin'
65
+ generate_test_file( test_filename, file_size )
66
+
67
+ buffer_size = file_size
68
+ buffer_count = 4
69
+ buffer_array.set_file( File.open( test_filename, 'rb' ), buffer_size: buffer_size, buffer_count: buffer_count )
70
+
71
+ assert_equal buffer_count, buffer_array.buffers.count
72
+ assert_equal 0, buffer_array.current_buffer_index
73
+
74
+ buffer_array.buffers.each do |buffer|
75
+ assert_equal false, buffer.empty?
76
+ assert_equal 0, buffer.read_position
77
+ assert_equal (buffer_size / buffer_count), buffer.write_position
78
+ assert_equal (buffer_size / buffer_count), buffer.data.size
79
+ end
80
+
81
+ file_size.times do |index|
82
+ assert_equal index, buffer_array.read(1).unpack('C').first
83
+ end
84
+
85
+ FileUtils.rm test_filename
86
+
87
+ end
88
+
89
+
90
+ def test_buffer_array_refill
91
+ buffer_array = Px4LogReader::LogBufferArray.new
92
+
93
+ file_size = 256
94
+ test_filename = './test_buffer_array_refill.bin'
95
+ generate_test_file( test_filename, file_size )
96
+
97
+ buffer_size = file_size / 2
98
+ buffer_count = 4
99
+ single_buffer_size = buffer_size / buffer_count
100
+ buffer_array.set_file( File.open( test_filename, 'rb' ), buffer_size: buffer_size, buffer_count: buffer_count )
101
+
102
+ assert_equal buffer_count, buffer_array.buffers.count
103
+ assert_equal 0, buffer_array.current_buffer_index
104
+
105
+ buffer_array.buffers.each do |buffer|
106
+ assert_equal false, buffer.empty?
107
+ assert_equal (buffer_size / buffer_count), buffer.write_position
108
+ assert_equal (buffer_size / buffer_count), buffer.data.size
109
+ end
110
+
111
+ data = ''
112
+
113
+ # Read enough data to empty the first buffer.
114
+ data << buffer_array.read( single_buffer_size + single_buffer_size / 2 )
115
+ assert buffer_array.buffers[0].empty?
116
+
117
+ # Refill the empty buffer.
118
+ buffer_array.load_empty_buffers
119
+ assert_equal false, buffer_array.buffers[0].empty?
120
+ data << buffer_array.read( single_buffer_size / 2 + 3 * single_buffer_size )
121
+ assert_equal 5 * single_buffer_size, data.size
122
+
123
+ # Verify that we cannot read any more data.
124
+ assert_equal '', buffer_array.read( single_buffer_size )
125
+
126
+ assert_equal true, validate_data( test_filename, data )
127
+
128
+
129
+ FileUtils.rm test_filename
130
+ end
131
+
132
+
133
+ def generate_test_file( path, size )
134
+ test_filename = './test_buffer_read_write.bin'
135
+ File.open( path, 'wb+' ) do |io|
136
+ value = 0
137
+ size.times do
138
+ io.write( [ value ].pack('C') )
139
+ value = ( value + 1 ) % 256
140
+ end
141
+ end
142
+ end
143
+
144
+ def validate_data( path, data, max_bytes = nil )
145
+ equal = true
146
+
147
+ max_bytes = data.size if max_bytes.nil?
148
+
149
+ data = data.unpack('C*')
150
+
151
+ File.open( path, 'rb' ) do |io|
152
+ comp_index = 0
153
+ while ( comp_index < max_bytes ) && equal do
154
+ begin
155
+ byte = io.read(1).unpack('C').first
156
+ if data[comp_index] != byte
157
+ puts "mismatch at offset=#{comp_index}: expected '#{"0x%02X"%byte}; found '#{"0x%02X"%data[comp_index]}'"
158
+ equal = false
159
+ end
160
+ rescue EOFError => error
161
+ puts "reached EOF before end of data"
162
+ equal = false
163
+ end
164
+ comp_index += 1
165
+ end
166
+ end
167
+
168
+ return equal
169
+ end
170
+
171
+ end
172
+
173
+