rplidar 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9c00e8000624de8044d80c1ecdcb65f99d9f806d60d684267b6a067176cf8bf3
4
+ data.tar.gz: c267e4e70b865ce84b0df3767e5c8ec43166a56e8bc678568e4e69c6eb455778
5
+ SHA512:
6
+ metadata.gz: 2b58e8eab346bee4bbea60436e363dd5f534e6ea473564904d7cc130bff9f8582ed4ab2c5eee59ab9450972d5db2bf857237941cbe800232440731bb47bd9313
7
+ data.tar.gz: db1f7b6f72e4baf940ac2b2fe0827d86b749164c59d4e6d4d6d3db89af465b2fdb1d5d5a1ebc8d03802d85a82a4eacc8dc46b896d0f0648d596fcd9074339ca1
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /coverage
2
+ /spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,9 @@
1
+ #Metrics/BlockLength:
2
+ # Exclude:
3
+ # - 'Rakefile'
4
+ # - '**/*.rake'
5
+ # - 'spec/**/*.rb'
6
+ require: rubocop-rspec
7
+
8
+ Layout/IndentArray:
9
+ EnforcedStyle: consistent
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in rplidar.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,72 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rplidar (0.1.0)
5
+ rubyserial (~> 0.6)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.0)
11
+ codecov (0.1.10)
12
+ json
13
+ simplecov
14
+ url
15
+ diff-lcs (1.3)
16
+ docile (1.3.0)
17
+ ffi (1.9.25)
18
+ jaro_winkler (1.5.1)
19
+ json (2.1.0)
20
+ parallel (1.12.1)
21
+ parser (2.5.1.2)
22
+ ast (~> 2.4.0)
23
+ powerpack (0.1.2)
24
+ rainbow (3.0.0)
25
+ rake (10.5.0)
26
+ rspec (3.7.0)
27
+ rspec-core (~> 3.7.0)
28
+ rspec-expectations (~> 3.7.0)
29
+ rspec-mocks (~> 3.7.0)
30
+ rspec-core (3.7.1)
31
+ rspec-support (~> 3.7.0)
32
+ rspec-expectations (3.7.0)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.7.0)
35
+ rspec-mocks (3.7.0)
36
+ diff-lcs (>= 1.2.0, < 2.0)
37
+ rspec-support (~> 3.7.0)
38
+ rspec-support (3.7.1)
39
+ rubocop (0.58.2)
40
+ jaro_winkler (~> 1.5.1)
41
+ parallel (~> 1.10)
42
+ parser (>= 2.5, != 2.5.1.1)
43
+ powerpack (~> 0.1)
44
+ rainbow (>= 2.2.2, < 4.0)
45
+ ruby-progressbar (~> 1.7)
46
+ unicode-display_width (~> 1.0, >= 1.0.1)
47
+ rubocop-rspec (1.29.0)
48
+ rubocop (>= 0.58.0)
49
+ ruby-progressbar (1.10.0)
50
+ rubyserial (0.6.0)
51
+ ffi (~> 1.9, >= 1.9.3)
52
+ simplecov (0.16.1)
53
+ docile (~> 1.1)
54
+ json (>= 1.8, < 3)
55
+ simplecov-html (~> 0.10.0)
56
+ simplecov-html (0.10.2)
57
+ unicode-display_width (1.4.0)
58
+ url (0.3.2)
59
+
60
+ PLATFORMS
61
+ ruby
62
+
63
+ DEPENDENCIES
64
+ bundler (~> 1.16)
65
+ codecov
66
+ rake (~> 10.0)
67
+ rplidar!
68
+ rspec (~> 3.0)
69
+ rubocop-rspec
70
+
71
+ BUNDLED WITH
72
+ 1.16.4
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Yury Kotlyarov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # Rplidar
2
+
3
+ [![Build Status](https://semaphoreci.com/api/v1/yurykotlyarov/rplidar/branches/master/shields_badge.svg)](https://semaphoreci.com/yurykotlyarov/rplidar) [![codecov](https://codecov.io/gh/yura/rplidar/branch/master/graph/badge.svg)](https://codecov.io/gh/yura/rplidar) [![Maintainability](https://api.codeclimate.com/v1/badges/3e73393095982858c97b/maintainability)](https://codeclimate.com/github/yura/rplidar/maintainability) [![security](https://hakiri.io/github/yura/rplidar/master.svg)](https://hakiri.io/github/yura/rplidar/master)
4
+
5
+ Ruby implementation of SLAMTEK RPLIDAR A2M8 lidar.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'rplidar'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install rplidar
22
+
23
+ ## Usage
24
+
25
+ Run `bundle exec irb`
26
+
27
+ ```ruby
28
+ require './lib/rplidar'
29
+
30
+ # for Mac OS
31
+ lidar = Rplidar::Driver.new('/dev/tty.SLAB_USBtoUART')
32
+ lidar.current_state
33
+
34
+ lidar.start_motor
35
+ lidar.scan
36
+ ...
37
+ lidar.stop
38
+ lidar.stop_motor
39
+ lidar.close
40
+ ```
41
+
42
+ ## Development
43
+
44
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
45
+
46
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
47
+
48
+ ## Contributing
49
+
50
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yura/rplidar. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
51
+
52
+ ## License
53
+
54
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
55
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'rplidar'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/rplidar.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'rplidar/driver'
2
+ require 'rplidar/version'
@@ -0,0 +1,215 @@
1
+ require 'rubyserial'
2
+
3
+ module Rplidar
4
+ # Ruby implementation of driver of the SLAMTEC RPLIDAR A2.
5
+ class Driver
6
+ # Lidar states
7
+ STATE_GOOD = 0
8
+ STATE_WARNING = 1
9
+ STATE_ERROR = 2
10
+
11
+ # Commands
12
+ COMMAND_GET_HEALTH = 0x52
13
+ COMMAND_MOTOR_PWM = 0xF0
14
+ COMMAND_SCAN = 0x20
15
+ COMMAND_STOP = 0x25
16
+ COMMAND_RESET = 0x40
17
+
18
+ COMMANDS_WITH_RESPONSE = [
19
+ COMMAND_GET_HEALTH,
20
+ COMMAND_SCAN
21
+ ].freeze
22
+
23
+ # Default length of responses
24
+ RESPONSE_DESCRIPTOR_LENGTH = 7
25
+ SCAN_DATA_RESPONSE_LENGTH = 5
26
+
27
+ UART_BAUD_RATE = 115_200
28
+
29
+ def initialize(port_address)
30
+ @port_address = port_address
31
+ end
32
+
33
+ def current_state
34
+ descriptor = command(COMMAND_GET_HEALTH)
35
+ response = data_response(descriptor[:data_response_length])
36
+ case response[0]
37
+ when STATE_GOOD then [:good, []]
38
+ when STATE_WARNING then [:warning, []]
39
+ when STATE_ERROR then [:error, response[1..-1]]
40
+ end
41
+ end
42
+
43
+ def start_motor(pwm = 660)
44
+ request_with_payload(COMMAND_MOTOR_PWM, pwm)
45
+ end
46
+
47
+ def stop_motor
48
+ request_with_payload(COMMAND_MOTOR_PWM, 0)
49
+ end
50
+
51
+ def scan_to_file(filename = 'output.csv', iterations = 1)
52
+ responses = scan(iterations)
53
+
54
+ File.open(filename, 'w') do |file|
55
+ file.puts 'start,quality,angle,distance'
56
+ responses.each do |r|
57
+ file.puts "#{r[:start]},#{r[:quality]},#{r[:angle]},#{r[:distance]}"
58
+ end
59
+ end
60
+ end
61
+
62
+ def scan(iterations = 1)
63
+ command(COMMAND_SCAN)
64
+ responses = collect_scan_data_responses(iterations)
65
+ stop
66
+
67
+ responses
68
+ end
69
+
70
+ def collect_scan_data_responses(iterations)
71
+ responses = []
72
+ iteration = -1
73
+ while iteration < iterations
74
+ response = scan_data_response
75
+ iteration += 1 if response[:start]
76
+ responses << response if iteration.between?(0, iterations - 1)
77
+ end
78
+ responses
79
+ end
80
+
81
+ def stop
82
+ command(COMMAND_STOP)
83
+ clear_port
84
+ end
85
+
86
+ def reset
87
+ command(COMMAND_RESET)
88
+ end
89
+
90
+ def port
91
+ @port ||= Serial.new(@port_address, UART_BAUD_RATE, 8, :none, 1)
92
+ end
93
+
94
+ def close
95
+ @port.close if @port
96
+ end
97
+
98
+ def command(command)
99
+ request(command)
100
+ response_descriptor if COMMANDS_WITH_RESPONSE.include?(command)
101
+ end
102
+
103
+ def request(command)
104
+ params = [0xA5, command]
105
+ port.write(ints_to_binary(params))
106
+ sleep 0.5
107
+ end
108
+
109
+ def request_with_payload(command, payload)
110
+ payload_string = ints_to_binary(payload, 'S<*')
111
+ payload_size = payload_string.size
112
+
113
+ string = ints_to_binary([0xA5, command, payload_size])
114
+ string += payload_string
115
+ string += ints_to_binary(checksum(string))
116
+
117
+ port.write(string)
118
+ end
119
+
120
+ def checksum(string)
121
+ binary_to_ints(string).reduce(:^)
122
+ end
123
+
124
+ # Format of Response Descriptor:
125
+ #
126
+ # Start Flag 1 Start Flag 2 Data Response Length Send Mode Data Type
127
+ #
128
+ # 1 byte (0xA5) 1 bytes (0x5A) 30 bits 2 bits 1 byte
129
+ def response_descriptor
130
+ response = data_response(RESPONSE_DESCRIPTOR_LENGTH)
131
+
132
+ # TODO: check response headers
133
+
134
+ {
135
+ data_response_length: (response[4] << 16) +
136
+ (response[3] << 8) + response[2],
137
+ send_mode: response[5] >> 6,
138
+ data_type: response[6]
139
+ }
140
+ end
141
+
142
+ def data_response(length)
143
+ start = Time.now
144
+ response = []
145
+ while response.size < length
146
+ byte = port.getbyte
147
+ response << byte if byte
148
+ raise 'Timeout while getting byte from the port' if Time.now - start > 2
149
+ end
150
+ response
151
+ end
152
+
153
+ def scan_data_response
154
+ response = data_response(SCAN_DATA_RESPONSE_LENGTH)
155
+ check_data_response_header(response)
156
+
157
+ {
158
+ start: response[0][0] == 1,
159
+ quality: quality(response),
160
+ angle: angle(response),
161
+ distance: distance(response)
162
+ }
163
+ end
164
+
165
+ def check_data_response_header(response)
166
+ unless correct_start_bit?(response)
167
+ raise 'Inversed start bit of the data response ' \
168
+ 'is not inverse of the start bit'
169
+ end
170
+
171
+ unless correct_check_bit?(response)
172
+ raise 'Check bit of the data response is not equal ' \
173
+ 'to 1'
174
+ end
175
+ end
176
+
177
+ def correct_start_bit?(response)
178
+ # start bit
179
+ start = response[0][0]
180
+ # inversed start bit
181
+ inversed = response[0][1]
182
+
183
+ (start == 1 && inversed.zero?) || (start.zero? && inversed == 1)
184
+ end
185
+
186
+ def correct_check_bit?(response)
187
+ response[1][0] == 1
188
+ end
189
+
190
+ def quality(response)
191
+ response[0] >> 2
192
+ end
193
+
194
+ def angle(response)
195
+ ((response[2] << 7) + (response[1] >> 1)) / 64.0
196
+ end
197
+
198
+ def distance(response)
199
+ ((response[4] << 8) + response[3]) / 4.0
200
+ end
201
+
202
+ def clear_port
203
+ while port.getbyte
204
+ end
205
+ end
206
+
207
+ def ints_to_binary(array, format = 'C*')
208
+ [array].flatten.pack(format)
209
+ end
210
+
211
+ def binary_to_ints(string, format = 'C*')
212
+ string.unpack(format)
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,3 @@
1
+ module Rplidar
2
+ VERSION = '0.1.2'.freeze
3
+ end
data/rplidar.gemspec ADDED
@@ -0,0 +1,45 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'rplidar/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'rplidar'
7
+ spec.version = Rplidar::VERSION
8
+ spec.authors = ['Yury Kotlyarov']
9
+ spec.email = ['yura@brainhouse.ru']
10
+
11
+ spec.summary = 'Ruby implementation of SLAMTEK RPLIDAR driver.'
12
+ spec.description = 'Tested on SLAMTEK RPLIDAR A2M8'
13
+ spec.homepage = 'http://github.com/yura/rplidar'
14
+ spec.license = 'MIT'
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the
17
+ # 'allowed_push_host' to allow pushing to a single host or delete this section
18
+ # to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
21
+ else
22
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
23
+ 'public gem pushes.'
24
+ end
25
+
26
+ # Specify which files should be added to the gem when it is released.
27
+ # The `git ls-files -z` loads the files in the RubyGem that have been
28
+ # added into git.
29
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
30
+ `git ls-files -z`
31
+ .split("\x0")
32
+ .reject { |f| f.match(%r{^(test|features)/}) }
33
+ end
34
+ spec.bindir = 'exe'
35
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
36
+ spec.require_paths = ['lib']
37
+
38
+ spec.add_dependency 'rubyserial', '~> 0.6'
39
+
40
+ spec.add_development_dependency 'bundler', '~> 1.16'
41
+ spec.add_development_dependency 'codecov'
42
+ spec.add_development_dependency 'rake', '~> 10.0'
43
+ spec.add_development_dependency 'rspec', '~> 3.0'
44
+ spec.add_development_dependency 'rubocop-rspec'
45
+ end
@@ -0,0 +1,447 @@
1
+ # do not convert string to unicode
2
+ def ascii(string)
3
+ string.force_encoding('ASCII-8BIT')
4
+ end
5
+
6
+ # Raw response descriptors
7
+ RAW_RD_GET_HEALTH = ascii("\xA5Z\x03\x00\x00\x00\x06")
8
+ RAW_RD_SCAN = ascii("\xA5Z\x05\x00\x00@\x81")
9
+
10
+ RD_GET_HEALTH = [165, 90, 3, 0, 0, 0, 6].freeze
11
+ RD_SCAN = [165, 90, 5, 0, 0, 64, 129].freeze
12
+
13
+ # Data Responses
14
+ DR_HEALTH_GOOD = [0, 0, 0].freeze
15
+ DR_HEALTH_WARNING = [1, 0, 0].freeze
16
+ DR_HEALTH_ERROR = [2, 3, 5].freeze
17
+
18
+ DR_SCAN = [62, 63, 3, 117, 4].freeze
19
+
20
+ RSpec.describe Rplidar::Driver do
21
+ let(:lidar) { described_class.new('/serial') }
22
+ let(:port) { instance_double('serial port') }
23
+
24
+ before do
25
+ allow(Serial).to receive(:new)
26
+ .with('/serial', 115_200, 8, :none, 1)
27
+ .and_return(port)
28
+ end
29
+
30
+ describe '#current_state' do
31
+ subject(:current_state) { lidar.current_state }
32
+
33
+ before do
34
+ allow(lidar).to receive(:command)
35
+ .with(0x52)
36
+ .and_return(data_response_length: 3)
37
+ allow(lidar).to receive(:data_response)
38
+ .with(3)
39
+ .and_return([0, 0, 0])
40
+ end
41
+
42
+ it 'sends GET_HEALTH command' do
43
+ current_state
44
+ expect(lidar).to have_received(:command).with(0x52)
45
+ end
46
+
47
+ it 'reads data_response' do
48
+ current_state
49
+ expect(lidar).to have_received(:data_response).with(3)
50
+ end
51
+
52
+ it 'returns :good if lidar is in Good (0) state' do
53
+ allow(lidar).to receive(:data_response)
54
+ .with(3)
55
+ .and_return(DR_HEALTH_GOOD)
56
+ expect(current_state).to eq([:good, []])
57
+ end
58
+
59
+ it 'returns :warning if lidar is in Warning (1) state' do
60
+ allow(lidar).to receive(:data_response)
61
+ .with(3)
62
+ .and_return(DR_HEALTH_WARNING)
63
+ expect(current_state).to eq([:warning, []])
64
+ end
65
+
66
+ it 'returns :error if lidar is in Error (2) state' do
67
+ allow(lidar).to receive(:data_response)
68
+ .with(3)
69
+ .and_return(DR_HEALTH_ERROR)
70
+ expect(current_state).to eq([:error, [3, 5]])
71
+ end
72
+
73
+ it 'concatenates error code bytes'
74
+ end
75
+
76
+ describe '#start_motor' do
77
+ subject(:start_motor) { lidar.start_motor }
78
+
79
+ before do
80
+ allow(lidar).to receive(:request_with_payload).with(0xF0, 660)
81
+ end
82
+
83
+ it 'sends START_MOTOR command' do
84
+ start_motor
85
+ expect(lidar).to have_received(:request_with_payload).with(0xF0, 660)
86
+ end
87
+ end
88
+
89
+ describe '#stop_motor' do
90
+ subject(:stop_motor) { lidar.stop_motor }
91
+
92
+ before do
93
+ allow(lidar).to receive(:request_with_payload)
94
+ .with(0xF0, 0).and_return(nil)
95
+ end
96
+
97
+ it 'sends STOP_MOTOR command' do
98
+ stop_motor
99
+ expect(lidar).to have_received(:request_with_payload).with(0xF0, 0)
100
+ end
101
+ end
102
+
103
+ describe '#scan' do
104
+ subject(:scan) { lidar.scan }
105
+
106
+ before do
107
+ allow(lidar).to receive(:command)
108
+ .with(0x20).and_return(data_response_length: 5)
109
+ allow(lidar).to receive(:collect_scan_data_responses)
110
+ .with(1).and_return([1, 2, 3, 4, 5])
111
+ allow(lidar).to receive(:stop)
112
+ end
113
+
114
+ it 'sends SCAN request' do
115
+ scan
116
+ expect(lidar).to have_received(:command).with(0x20)
117
+ end
118
+
119
+ it 'collects scan data responses' do
120
+ scan
121
+ expect(lidar).to have_received(:collect_scan_data_responses).with(1)
122
+ end
123
+
124
+ it 'stops scanning' do
125
+ scan
126
+ expect(lidar).to have_received(:stop)
127
+ end
128
+ end
129
+
130
+ describe '#collect_scan_data_responses' do
131
+ subject(:collect_scans) { lidar.collect_scan_data_responses(1) }
132
+
133
+ before do
134
+ allow(lidar).to receive(:scan_data_response)
135
+ .and_return(
136
+ { start: true, angle: 110, distance: 220, quality: 10 },
137
+ { start: false, angle: 111, distance: 230, quality: 11 },
138
+ start: true, angle: 112, distance: 240, quality: 12
139
+ )
140
+ end
141
+
142
+ it 'calls :scan_data_response' do
143
+ collect_scans
144
+ expect(lidar).to have_received(:scan_data_response).exactly(3).times
145
+ end
146
+
147
+ it 'returns collected iterations' do
148
+ expect(collect_scans).to eq([
149
+ { start: true, angle: 110, distance: 220, quality: 10 },
150
+ { start: false, angle: 111, distance: 230, quality: 11 }
151
+ ])
152
+ end
153
+ end
154
+
155
+ describe '#stop' do
156
+ subject(:stop) { lidar.stop }
157
+
158
+ before do
159
+ allow(lidar).to receive(:request).with(0x25)
160
+ allow(lidar).to receive(:clear_port)
161
+ end
162
+
163
+ it 'sends STOP request' do
164
+ stop
165
+ expect(lidar).to have_received(:request).with(0x25)
166
+ end
167
+
168
+ it 'sleeps for at least 1 ms'
169
+
170
+ it 'clears port afterwards' do
171
+ stop
172
+ expect(lidar).to have_received(:clear_port)
173
+ end
174
+ end
175
+
176
+ describe '#reset' do
177
+ subject(:reset) { lidar.reset }
178
+
179
+ before do
180
+ allow(lidar).to receive(:request).with(0x40)
181
+ end
182
+
183
+ it 'sends RESET request' do
184
+ reset
185
+ expect(lidar).to have_received(:request).with(0x40)
186
+ end
187
+
188
+ it 'sleeps for at least 2 ms'
189
+ end
190
+
191
+ describe '#request' do
192
+ subject(:request) { lidar.request(0x20) }
193
+
194
+ before do
195
+ allow(lidar).to receive(:port).and_return(port)
196
+ allow(port).to receive(:write).with(ascii("\xA5 "))
197
+ end
198
+
199
+ it 'gets serial port' do
200
+ request
201
+ expect(lidar).to have_received(:port)
202
+ end
203
+
204
+ it 'writes binary string to the serial port' do
205
+ request
206
+ expect(port).to have_received(:write).with(ascii("\xA5 "))
207
+ end
208
+ end
209
+
210
+ describe '#response_descriptor' do
211
+ before do
212
+ allow(lidar).to receive(:data_response)
213
+ .with(7)
214
+ .and_return(RD_GET_HEALTH)
215
+ end
216
+
217
+ it 'reads 7 bytes from the port' do
218
+ lidar.response_descriptor
219
+ expect(lidar).to have_received(:data_response).with(7)
220
+ end
221
+
222
+ it 'processes GET_HEALTH response descriptor correctly' do
223
+ allow(lidar).to receive(:data_response)
224
+ .with(7).and_return(RD_GET_HEALTH)
225
+ expect(lidar.response_descriptor).to \
226
+ eq(data_response_length: 3, send_mode: 0, data_type: 6)
227
+ end
228
+
229
+ it 'processes scan response descriptor correctly' do
230
+ allow(lidar).to receive(:data_response)
231
+ .with(7).and_return(RD_SCAN)
232
+ expect(lidar.response_descriptor).to \
233
+ eq(data_response_length: 5, send_mode: 1, data_type: 129)
234
+ end
235
+ end
236
+
237
+ describe '#request_with_payload' do
238
+ subject(:request_with_payload) { lidar.request_with_payload(0xF0, 660) }
239
+
240
+ before do
241
+ allow(port).to receive(:write).with(ascii("\xA5\xF0\x02\x94\x02\xC1"))
242
+ end
243
+
244
+ it 'writes binary string with payload to the serial port' do
245
+ request_with_payload
246
+ expect(port).to have_received(:write)
247
+ .with(ascii("\xA5\xF0\x02\x94\x02\xC1"))
248
+ end
249
+ end
250
+
251
+ describe '#checksum' do
252
+ it 'XORs one byte sequence' do
253
+ expect(lidar.checksum('a')).to eq(97)
254
+ end
255
+
256
+ it 'XORs few bytes' do
257
+ expect(lidar.checksum("\xA5\xF0")).to eq(0xA5 ^ 0xF0)
258
+ end
259
+ end
260
+
261
+ describe '#data_response' do
262
+ subject(:data_response) { lidar.data_response(5) }
263
+
264
+ before do
265
+ allow(port).to receive(:getbyte).and_return(1, 55, 88, 111, 222, 111)
266
+ end
267
+
268
+ it 'reads bytes from the port' do
269
+ data_response
270
+ expect(port).to have_received(:getbyte).exactly(5).times
271
+ end
272
+
273
+ it 'returns all read bytes' do
274
+ expect(data_response).to eq([1, 55, 88, 111, 222])
275
+ end
276
+
277
+ it 'raises timeout exception if read takes more than 2 seconds' do
278
+ allow(port).to receive(:getbyte).and_return(1, 2, 3, nil)
279
+ expect do
280
+ data_response
281
+ end.to raise_error('Timeout while getting byte from the port')
282
+ end
283
+ end
284
+
285
+ describe '#scan_data_response' do
286
+ subject(:scan_data_response) { lidar.scan_data_response }
287
+
288
+ before do
289
+ allow(lidar).to receive(:data_response).with(5).and_return(DR_SCAN)
290
+ allow(lidar).to receive(:check_data_response_header).with(DR_SCAN)
291
+ allow(lidar).to receive(:angle).with(DR_SCAN).and_return(111)
292
+ allow(lidar).to receive(:distance).with(DR_SCAN).and_return(222)
293
+ allow(lidar).to receive(:quality).with(DR_SCAN).and_return(333)
294
+ end
295
+
296
+ it 'reads data_response' do
297
+ scan_data_response
298
+ expect(lidar).to have_received(:data_response).with(5)
299
+ end
300
+
301
+ it 'checks headers' do
302
+ scan_data_response
303
+ expect(lidar).to have_received(:check_data_response_header).with(DR_SCAN)
304
+ end
305
+
306
+ it 'returns hash with processed values' do
307
+ expect(scan_data_response).to eq(
308
+ start: false, angle: 111, distance: 222, quality: 333
309
+ )
310
+ end
311
+ end
312
+
313
+ describe '#check_data_response_header' do
314
+ it 'raises inversed start flag bit is not inverse of the start flag bit' do
315
+ [[[1, 1]], [[0, 0]], [[1, -2]], [[0, -1]]].each do |wrong_response|
316
+ expect { lidar.check_data_response_header(wrong_response) }.to \
317
+ raise_error('Inversed start bit of the data response ' \
318
+ 'is not inverse of the start bit')
319
+ end
320
+ end
321
+
322
+ it 'raises an exception if 3rd bit is not equal to 1' do
323
+ [[[1, 0], [0]], [[0, 1], [2]]].each do |wrong_response|
324
+ expect { lidar.check_data_response_header(wrong_response) }.to \
325
+ raise_error('Check bit of the data response is not equal to 1')
326
+ end
327
+ end
328
+ end
329
+
330
+ describe '#close' do
331
+ subject(:close) { lidar.close }
332
+
333
+ before do
334
+ allow(port).to receive(:close)
335
+ end
336
+
337
+ it 'does not close the port if it is not open' do
338
+ close
339
+ expect(port).not_to have_received(:close)
340
+ end
341
+
342
+ it 'closes the port if it is exist' do
343
+ # open create port first
344
+ lidar.port
345
+
346
+ close
347
+ expect(port).to have_received(:close)
348
+ end
349
+ end
350
+
351
+ describe '#port' do
352
+ it 'opens serial port' do
353
+ lidar.port
354
+ expect(Serial).to have_received(:new)
355
+ .with('/serial', 115_200, 8, :none, 1)
356
+ end
357
+
358
+ it 'does not open port if it is already open' do
359
+ # call it first time
360
+ lidar.port
361
+
362
+ # call it second time
363
+ lidar.port
364
+ expect(Serial).to have_received(:new).with(any_args).once
365
+ end
366
+ end
367
+
368
+ describe '#correct_start_bit?' do
369
+ it 'raises inversed start flag bit is not inverse of the start flag bit' do
370
+ [[[1, 1]], [[0, 0]], [[1, -2]], [[0, -1]]].each do |response|
371
+ expect(lidar.correct_start_bit?(response)).to be_falsy
372
+ end
373
+ end
374
+ end
375
+
376
+ describe '#correct_check_bit?' do
377
+ it 'returns true if first bit of the second byte is equal to 1' do
378
+ [[0, 1], [0, 3], [0, 5], [0, 255]].each do |response|
379
+ expect(lidar.correct_check_bit?(response)).to be_truthy
380
+ end
381
+ end
382
+
383
+ it 'returns false if 1st bit of the 2nd byte is not equal to 1' do
384
+ [[0, 0], [0, 2], [0, 254]].each do |response|
385
+ expect(lidar.correct_check_bit?(response)).to be_falsy
386
+ end
387
+ end
388
+ end
389
+
390
+ describe '#angle' do
391
+ it 'processes angle from the 2nd and 3rd bytes' do
392
+ expect(lidar.angle([62, 155, 2, 112, 4])).to eq(5.203125)
393
+ end
394
+ end
395
+
396
+ describe '#distance' do
397
+ it 'processes angle from the 4th and 5th bytes' do
398
+ expect(lidar.distance([62, 155, 2, 112, 4])).to eq(284)
399
+ end
400
+ end
401
+
402
+ describe '#quality' do
403
+ it 'processes quantity from the 1st bit' do
404
+ expect(lidar.quality([62, 155, 2, 112, 4])).to eq(15)
405
+ end
406
+ end
407
+
408
+ describe '#clear_port' do
409
+ subject(:clear_port) { lidar.clear_port }
410
+
411
+ before do
412
+ allow(lidar).to receive(:port).and_return(port)
413
+ allow(port).to receive(:getbyte).and_return(1, 2, 3, 4, 5, nil)
414
+ end
415
+
416
+ it 'gets port' do
417
+ clear_port
418
+ expect(lidar).to have_received(:port).exactly(6).times
419
+ end
420
+
421
+ it 'reads all bytes from the port' do
422
+ clear_port
423
+ expect(port).to have_received(:getbyte).exactly(6).times
424
+ end
425
+ end
426
+
427
+ describe '#binary_to_ints' do
428
+ it 'converts binary sequence to integer array' do
429
+ expect(lidar.binary_to_ints(RAW_RD_GET_HEALTH)).to \
430
+ eq([165, 90, 3, 0, 0, 0, 6])
431
+ end
432
+ end
433
+
434
+ describe '#ints_to_binary' do
435
+ it 'coverts integer to the binary sequence' do
436
+ expect(lidar.ints_to_binary(97)).to eq('a')
437
+ end
438
+
439
+ it 'converts array with one integer to the binary sequence' do
440
+ expect(lidar.ints_to_binary([97])).to eq('a')
441
+ end
442
+
443
+ it 'converts array of integers to the binary sequence' do
444
+ expect(lidar.ints_to_binary([0xA5, 0x20])).to eq(ascii("\xA5 "))
445
+ end
446
+ end
447
+ end
@@ -0,0 +1,107 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ if ENV['CI']
5
+ require 'codecov'
6
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
7
+ end
8
+
9
+ require 'bundler/setup'
10
+ require 'rplidar'
11
+
12
+ # This file was generated by the `rspec --init` command. Conventionally, all
13
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
14
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
15
+ # this file to always be loaded, without a need to explicitly require it in any
16
+ # files.
17
+ #
18
+ # Given that it is always loaded, you are encouraged to keep this file as
19
+ # light-weight as possible. Requiring heavyweight dependencies from this file
20
+ # will add to the boot time of your test suite on EVERY test run, even for an
21
+ # individual file that may not need all of that loaded. Instead, consider making
22
+ # a separate helper file that requires the additional dependencies and performs
23
+ # the additional setup, and require it from the spec files that actually need
24
+ # it.
25
+ #
26
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
27
+ RSpec.configure do |config|
28
+ # rspec-expectations config goes here. You can use an alternate
29
+ # assertion/expectation library such as wrong or the stdlib/minitest
30
+ # assertions if you prefer.
31
+ config.expect_with :rspec do |expectations|
32
+ # This option will default to `true` in RSpec 4. It makes the `description`
33
+ # and `failure_message` of custom matchers include text for helper methods
34
+ # defined using `chain`, e.g.:
35
+ # be_bigger_than(2).and_smaller_than(4).description
36
+ # # => "be bigger than 2 and smaller than 4"
37
+ # ...rather than:
38
+ # # => "be bigger than 2"
39
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
40
+ end
41
+
42
+ # rspec-mocks config goes here. You can use an alternate test double
43
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
44
+ config.mock_with :rspec do |mocks|
45
+ # Prevents you from mocking or stubbing a method that does not exist on
46
+ # a real object. This is generally recommended, and will default to
47
+ # `true` in RSpec 4.
48
+ mocks.verify_partial_doubles = true
49
+ end
50
+
51
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
52
+ # have no way to turn it off -- the option exists only for backwards
53
+ # compatibility in RSpec 3). It causes shared context metadata to be
54
+ # inherited by the metadata hash of host groups and examples, rather than
55
+ # triggering implicit auto-inclusion in groups with matching metadata.
56
+ config.shared_context_metadata_behavior = :apply_to_host_groups
57
+
58
+ # This allows you to limit a spec run to individual examples or groups
59
+ # you care about by tagging them with `:focus` metadata. When nothing
60
+ # is tagged with `:focus`, all examples get run. RSpec also provides
61
+ # aliases for `it`, `describe`, and `context` that include `:focus`
62
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
63
+ config.filter_run_when_matching :focus
64
+
65
+ # Allows RSpec to persist some state between runs in order to support
66
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
67
+ # you configure your source control system to ignore this file.
68
+ config.example_status_persistence_file_path = 'spec/examples.txt'
69
+
70
+ # Limits the available syntax to the non-monkey patched syntax that is
71
+ # recommended. For more details, see:
72
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
73
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
74
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
75
+ config.disable_monkey_patching!
76
+
77
+ # This setting enables warnings. It's recommended, but in some cases may
78
+ # be too noisy due to issues in dependencies.
79
+ config.warnings = true
80
+
81
+ # Many RSpec users commonly either run the entire suite or an individual
82
+ # file, and it's useful to allow more verbose output when running an
83
+ # individual spec file.
84
+ if config.files_to_run.one?
85
+ # Use the documentation formatter for detailed output,
86
+ # unless a formatter has already been configured
87
+ # (e.g. via a command-line flag).
88
+ config.default_formatter = 'doc'
89
+ end
90
+
91
+ # Print the 10 slowest examples and example groups at the
92
+ # end of the spec run, to help surface which specs are running
93
+ # particularly slow.
94
+ config.profile_examples = 10
95
+
96
+ # Run specs in random order to surface order dependencies. If you find an
97
+ # order dependency and want to debug it, you can fix the order by providing
98
+ # the seed, which is printed after each run.
99
+ # --seed 1234
100
+ config.order = :random
101
+
102
+ # Seed global randomization in this process using the `--seed` CLI option.
103
+ # Setting this allows you to use `--seed` to deterministically reproduce
104
+ # test failures related to randomization by passing the same `--seed` value
105
+ # as the one that triggered the failure.
106
+ Kernel.srand config.seed
107
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rplidar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Yury Kotlyarov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-09-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rubyserial
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: codecov
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Tested on SLAMTEK RPLIDAR A2M8
98
+ email:
99
+ - yura@brainhouse.ru
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".rubocop.yml"
107
+ - Gemfile
108
+ - Gemfile.lock
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - bin/console
113
+ - bin/setup
114
+ - lib/rplidar.rb
115
+ - lib/rplidar/driver.rb
116
+ - lib/rplidar/version.rb
117
+ - rplidar.gemspec
118
+ - spec/rplidar/driver_spec.rb
119
+ - spec/spec_helper.rb
120
+ homepage: http://github.com/yura/rplidar
121
+ licenses:
122
+ - MIT
123
+ metadata:
124
+ allowed_push_host: https://rubygems.org
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 2.7.3
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Ruby implementation of SLAMTEK RPLIDAR driver.
145
+ test_files: []