rplidar 0.1.2

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 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: []