px4_log_reader 0.0.5

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.
@@ -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
+