sphero_pwn 0.0.0
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 +7 -0
- data/.document +5 -0
- data/.travis.yml +5 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +76 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +25 -0
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/lib/sphero_pwn.rb +20 -0
- data/lib/sphero_pwn/async.rb +10 -0
- data/lib/sphero_pwn/asyncs.rb +34 -0
- data/lib/sphero_pwn/asyncs/l1_diagnostics.rb +21 -0
- data/lib/sphero_pwn/channel.rb +65 -0
- data/lib/sphero_pwn/channel_recorder.rb +60 -0
- data/lib/sphero_pwn/command.rb +54 -0
- data/lib/sphero_pwn/commands.rb +3 -0
- data/lib/sphero_pwn/commands/get_versions.rb +44 -0
- data/lib/sphero_pwn/commands/l1_diagnostics.rb +16 -0
- data/lib/sphero_pwn/commands/ping.rb +15 -0
- data/lib/sphero_pwn/replay_channel.rb +81 -0
- data/lib/sphero_pwn/response.rb +47 -0
- data/lib/sphero_pwn/session.rb +160 -0
- data/lib/sphero_pwn/test_channel.rb +18 -0
- data/test/channel_recorder_test.rb +102 -0
- data/test/command_test.rb +35 -0
- data/test/commands/get_versions_test.rb +34 -0
- data/test/commands/l1_diagnostics_test.rb +40 -0
- data/test/commands/ping_test.rb +25 -0
- data/test/data/get_version.txt +2 -0
- data/test/data/l1_diagnostics.txt +2 -0
- data/test/data/ping.txt +2 -0
- data/test/helper.rb +37 -0
- data/test/replay_channel_test.rb +147 -0
- data/test/response_test.rb +15 -0
- data/test/session_test.rb +15 -0
- data/test/sphero_pwn_test.rb +4 -0
- metadata +181 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6b51f0b12bbd766e4ad620a00be1d1841b90fe72
|
4
|
+
data.tar.gz: c23728318daf4e7e42d756b195461c39f3cdd0be
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 033e26a33d83a0e8a8ee49f38a43a69b75f975dbf81e2252055d1383aa5e96b275ebfb8ef1a49aa0d7d8961073eff0ec9d6f05e378528ac6f9cb492409cf0caf
|
7
|
+
data.tar.gz: 409bd796603489498346cab6f01caff53231c78a9e05d34a3302a0eb4f1b24208e7b1ef591da29791b56b4f6789f212cdd8ae83a53c6e1627317759b3906a242
|
data/.document
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'rubyserial', '>= 0.2.4'
|
4
|
+
|
5
|
+
group :development do
|
6
|
+
gem 'bundler', '>= 1.3.5'
|
7
|
+
gem 'jeweler', '>= 2.0.1'
|
8
|
+
gem 'minitest', '>= 5.8.3'
|
9
|
+
gem 'mocha', '>= 1.1.0'
|
10
|
+
gem 'simplecov', '>= 0', platform: :mri
|
11
|
+
gem 'yard', '>= 0.8.7.6'
|
12
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
addressable (2.4.0)
|
5
|
+
builder (3.2.2)
|
6
|
+
descendants_tracker (0.0.4)
|
7
|
+
thread_safe (~> 0.3, >= 0.3.1)
|
8
|
+
docile (1.1.5)
|
9
|
+
faraday (0.9.2)
|
10
|
+
multipart-post (>= 1.2, < 3)
|
11
|
+
ffi (1.9.10)
|
12
|
+
git (1.2.9.1)
|
13
|
+
github_api (0.13.0)
|
14
|
+
addressable (~> 2.3)
|
15
|
+
descendants_tracker (~> 0.0.4)
|
16
|
+
faraday (~> 0.8, < 0.10)
|
17
|
+
hashie (>= 3.4)
|
18
|
+
multi_json (>= 1.7.5, < 2.0)
|
19
|
+
nokogiri (~> 1.6.6)
|
20
|
+
oauth2
|
21
|
+
hashie (3.4.3)
|
22
|
+
highline (1.7.8)
|
23
|
+
jeweler (2.0.1)
|
24
|
+
builder
|
25
|
+
bundler (>= 1.0)
|
26
|
+
git (>= 1.2.5)
|
27
|
+
github_api
|
28
|
+
highline (>= 1.6.15)
|
29
|
+
nokogiri (>= 1.5.10)
|
30
|
+
rake
|
31
|
+
rdoc
|
32
|
+
json (1.8.3)
|
33
|
+
jwt (1.5.2)
|
34
|
+
metaclass (0.0.4)
|
35
|
+
mini_portile2 (2.0.0)
|
36
|
+
minitest (5.8.3)
|
37
|
+
mocha (1.1.0)
|
38
|
+
metaclass (~> 0.0.1)
|
39
|
+
multi_json (1.11.2)
|
40
|
+
multi_xml (0.5.5)
|
41
|
+
multipart-post (2.0.0)
|
42
|
+
nokogiri (1.6.7)
|
43
|
+
mini_portile2 (~> 2.0.0.rc2)
|
44
|
+
oauth2 (1.0.0)
|
45
|
+
faraday (>= 0.8, < 0.10)
|
46
|
+
jwt (~> 1.0)
|
47
|
+
multi_json (~> 1.3)
|
48
|
+
multi_xml (~> 0.5)
|
49
|
+
rack (~> 1.2)
|
50
|
+
rack (1.6.4)
|
51
|
+
rake (10.4.2)
|
52
|
+
rdoc (4.2.0)
|
53
|
+
rubyserial (0.2.4)
|
54
|
+
ffi (~> 1.9.3)
|
55
|
+
simplecov (0.11.1)
|
56
|
+
docile (~> 1.1.0)
|
57
|
+
json (~> 1.8)
|
58
|
+
simplecov-html (~> 0.10.0)
|
59
|
+
simplecov-html (0.10.0)
|
60
|
+
thread_safe (0.3.5)
|
61
|
+
yard (0.8.7.6)
|
62
|
+
|
63
|
+
PLATFORMS
|
64
|
+
ruby
|
65
|
+
|
66
|
+
DEPENDENCIES
|
67
|
+
bundler (>= 1.3.5)
|
68
|
+
jeweler (>= 2.0.1)
|
69
|
+
minitest (>= 5.8.3)
|
70
|
+
mocha (>= 1.1.0)
|
71
|
+
rubyserial (>= 0.2.4)
|
72
|
+
simplecov
|
73
|
+
yard (>= 0.8.7.6)
|
74
|
+
|
75
|
+
BUNDLED WITH
|
76
|
+
1.10.6
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2015 Victor Costan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# sphero_pwn
|
2
|
+
|
3
|
+
Wrapper around the
|
4
|
+
[Bluetooth RFCONN protocol](http://sdk.sphero.com/api-reference/api-packet-format/)
|
5
|
+
used to talk to
|
6
|
+
[Sphero robots](http://www.sphero.com/).
|
7
|
+
|
8
|
+
## Contributing to sphero_pwn
|
9
|
+
|
10
|
+
* Check out the latest master to make sure the feature hasn't been implemented
|
11
|
+
or the bug hasn't been fixed yet.
|
12
|
+
* Check out the issue tracker to make sure someone already hasn't requested it
|
13
|
+
and/or contributed it.
|
14
|
+
* Fork the project.
|
15
|
+
* Start a feature/bugfix branch.
|
16
|
+
* Commit and push until you are happy with your contribution.
|
17
|
+
* Make sure to add tests for it. This is important so I don't break it in a
|
18
|
+
future version unintentionally.
|
19
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to
|
20
|
+
have your own version, or is otherwise necessary, that is fine, but please
|
21
|
+
isolate to its own commit so I can cherry-pick around it.
|
22
|
+
|
23
|
+
## Copyright
|
24
|
+
|
25
|
+
Copyright (c) 2015 Victor Costan. See LICENSE.txt for further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
17
|
+
gem.name = "sphero_pwn"
|
18
|
+
gem.homepage = "http://github.com/pwnall/sphero_pwn"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Wrapper for Sphero's bluetooth protocol}
|
21
|
+
gem.description = %Q{This library is currently focused on reverse-engineering
|
22
|
+
the undocumented parts of Sphero}
|
23
|
+
gem.email = "victor@costan.us"
|
24
|
+
gem.authors = ["Victor Costan"]
|
25
|
+
# dependencies defined in Gemfile
|
26
|
+
end
|
27
|
+
Jeweler::RubygemsDotOrgTasks.new
|
28
|
+
|
29
|
+
require 'rake/testtask'
|
30
|
+
Rake::TestTask.new(:test) do |test|
|
31
|
+
test.libs << 'lib' << 'test'
|
32
|
+
test.pattern = 'test/**/*_test.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
|
36
|
+
task :default => :test
|
37
|
+
|
38
|
+
require 'yard'
|
39
|
+
YARD::Rake::YardocTask.new
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/lib/sphero_pwn.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Namespace.
|
2
|
+
module SpheroPwn
|
3
|
+
end
|
4
|
+
|
5
|
+
require 'sphero_pwn/async.rb'
|
6
|
+
require 'sphero_pwn/asyncs.rb'
|
7
|
+
require 'sphero_pwn/channel.rb'
|
8
|
+
require 'sphero_pwn/channel_recorder.rb'
|
9
|
+
require 'sphero_pwn/command.rb'
|
10
|
+
require 'sphero_pwn/commands.rb'
|
11
|
+
require 'sphero_pwn/replay_channel.rb'
|
12
|
+
require 'sphero_pwn/response.rb'
|
13
|
+
require 'sphero_pwn/session.rb'
|
14
|
+
require 'sphero_pwn/test_channel.rb'
|
15
|
+
|
16
|
+
require 'sphero_pwn/asyncs/l1_diagnostics.rb'
|
17
|
+
|
18
|
+
require 'sphero_pwn/commands/get_versions.rb'
|
19
|
+
require 'sphero_pwn/commands/l1_diagnostics.rb'
|
20
|
+
require 'sphero_pwn/commands/ping.rb'
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Namespace for asynchronous messages sent from the robot to the computer.
|
2
|
+
module SpheroPwn::Asyncs
|
3
|
+
# Called by SpheroPwn::Async subclasses to register themselves.
|
4
|
+
#
|
5
|
+
# @param {Class<SpheroPwn::Async>} klass a class that parses asynchronous
|
6
|
+
# messages with an ID code
|
7
|
+
def self.register(klass)
|
8
|
+
id_code = klass.id_code
|
9
|
+
if other_klass = CLASSES[id_code]
|
10
|
+
raise ArgumentError,
|
11
|
+
"Async ID code #{id_code} already registered by #{other_klass}"
|
12
|
+
end
|
13
|
+
CLASSES[id_code] = klass
|
14
|
+
end
|
15
|
+
|
16
|
+
# Subclasses override this and return the ID of the commands they can parse.
|
17
|
+
#
|
18
|
+
# @return {Number} the ID byte value identifying the commands that can be
|
19
|
+
# parsed by this class
|
20
|
+
def self.id_code
|
21
|
+
raise RuntimeError, 'id_code must be implemented by subclasses'
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param {Number} class_id the asynchronous message's ID code
|
25
|
+
# @return {Class<SpheroPwn::Async>} the class that can parse
|
26
|
+
def self.create(class_id, data_bytes)
|
27
|
+
return nil unless klass = CLASSES[class_id]
|
28
|
+
klass.new data_bytes
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# Maps ID codes to classes handling responses.
|
33
|
+
CLASSES = {}
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# The result of an L1 diagnostic request.
|
2
|
+
#
|
3
|
+
# This is an asynchronous message because it's too long to fit into the command
|
4
|
+
# response structure.
|
5
|
+
class SpheroPwn::Asyncs::L1Diagnostics < SpheroPwn::Async
|
6
|
+
# @return {String} the text form of the diagnostics
|
7
|
+
attr_reader :text
|
8
|
+
|
9
|
+
def initialize(data_bytes)
|
10
|
+
super
|
11
|
+
|
12
|
+
@text = data_bytes.pack('C*').encode! Encoding::UTF_8
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def self.id_code
|
17
|
+
0x02
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
SpheroPwn::Asyncs.register SpheroPwn::Asyncs::L1Diagnostics
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'rubyserial'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
# Communication channel with a robot.
|
5
|
+
#
|
6
|
+
# This is a light abstraction over the Bluetooth serial port (RFCONN) used to
|
7
|
+
# talk to a robot.
|
8
|
+
class SpheroPwn::Channel
|
9
|
+
# Opens up a communication channel with a robot.
|
10
|
+
#
|
11
|
+
# @param {String} rfconn_path the path to the device file connecting to the
|
12
|
+
# robot's Bluetooth RFCONN service
|
13
|
+
def initialize(rfconn_path)
|
14
|
+
give_up_at = Time.now + 15
|
15
|
+
@port = nil
|
16
|
+
while @port.nil?
|
17
|
+
begin
|
18
|
+
@port = Serial.new rfconn_path, 115200
|
19
|
+
rescue RubySerial::Exception => e
|
20
|
+
raise e unless e.message == 'EBUSY'
|
21
|
+
raise e if Time.now >= give_up_at
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
@send_queue = Queue.new
|
26
|
+
@send_thread = Thread.new @send_queue do
|
27
|
+
send_queue = @send_queue
|
28
|
+
|
29
|
+
loop do
|
30
|
+
bytes = send_queue.pop
|
31
|
+
break if bytes == :close
|
32
|
+
|
33
|
+
@port.write bytes
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param {String} bytes a binary-encoded string of bytes to be sent to the
|
39
|
+
# robot over the RFCONN port
|
40
|
+
# @return {Channel} self
|
41
|
+
def send_bytes(bytes)
|
42
|
+
@send_queue.push bytes
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param {Integer} count the number of bytes to be read from the RFCONN port
|
47
|
+
def recv_bytes(count)
|
48
|
+
retries_left = 100_000
|
49
|
+
while retries_left > 0
|
50
|
+
bytes = @port.read count
|
51
|
+
return bytes unless bytes.empty?
|
52
|
+
retries_left -= 1
|
53
|
+
end
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# Gracefully shuts down the communication channel with the robot.
|
58
|
+
#
|
59
|
+
# @return {Channel} self
|
60
|
+
def close
|
61
|
+
@send_queue.push :close
|
62
|
+
@port.close
|
63
|
+
self
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Records all the bytes going to a channel.
|
2
|
+
class SpheroPwn::ChannelRecorder
|
3
|
+
# @param {Channel} the channel being recorded
|
4
|
+
# @param {String} recording_path the file where the recording will be saved
|
5
|
+
def initialize(channel, recording_path)
|
6
|
+
@channel = channel
|
7
|
+
@file = File.open recording_path, 'wt'
|
8
|
+
|
9
|
+
@is_receiving = false
|
10
|
+
end
|
11
|
+
|
12
|
+
# @see {Channel#recv_bytes}
|
13
|
+
def recv_bytes(count)
|
14
|
+
bytes = @channel.recv_bytes count
|
15
|
+
return bytes if bytes.nil?
|
16
|
+
|
17
|
+
unless @is_receiving
|
18
|
+
@file.write '<'
|
19
|
+
@is_receiving = true
|
20
|
+
end
|
21
|
+
log_bytes bytes
|
22
|
+
@file.flush
|
23
|
+
|
24
|
+
bytes
|
25
|
+
end
|
26
|
+
|
27
|
+
# @see {Channel#send_bytes}
|
28
|
+
def send_bytes(bytes)
|
29
|
+
if @is_receiving
|
30
|
+
@file.write "\n"
|
31
|
+
@is_receiving = false
|
32
|
+
end
|
33
|
+
|
34
|
+
@file.write '>'
|
35
|
+
log_bytes bytes
|
36
|
+
@file.write "\n"
|
37
|
+
@file.flush
|
38
|
+
|
39
|
+
@channel.send_bytes bytes
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# @see {Channel#close}
|
44
|
+
def close
|
45
|
+
if @is_receiving
|
46
|
+
@file.write "\n"
|
47
|
+
end
|
48
|
+
@file.close
|
49
|
+
@channel.close
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param {String} bytes the bytes to be written to the output file
|
54
|
+
def log_bytes(bytes)
|
55
|
+
unless bytes.empty?
|
56
|
+
@file.write bytes.unpack('C*').map { |byte| ' %02X' % byte }.join('')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
private :log_bytes
|
60
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Superclass for the command messages going from the computer to the robot.
|
2
|
+
class SpheroPwn::Command
|
3
|
+
# @param {Number} device_id the virtual device ID for the command
|
4
|
+
# @param {Number} command_id the command number; unique within a virtual
|
5
|
+
# device ID
|
6
|
+
# @param {String} data_bytes extra data in the command; can be nil if no
|
7
|
+
# extra data will be transmitted
|
8
|
+
def initialize(device_id, command_id, data)
|
9
|
+
@device_id = device_id
|
10
|
+
@command_id = command_id
|
11
|
+
@data = data
|
12
|
+
@sop2 = 0xFF
|
13
|
+
end
|
14
|
+
|
15
|
+
# Clears the command bit that asks for a response.
|
16
|
+
def no_response!
|
17
|
+
@sop2 &= 0xFE
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Clears the command bit that resets the client inactivity timeout.
|
22
|
+
def no_timeout_reset!
|
23
|
+
@sop2 &= 0xFD
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return {Boolean} true if the command will receive a response
|
28
|
+
def expects_response?
|
29
|
+
(@sop2 & 0x01) != 0
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param {Number} the sequence number to be embedded in the command
|
33
|
+
def to_bytes(sequence)
|
34
|
+
data_length = @data.nil? ? 1 : 1 + @data.length
|
35
|
+
data_length = 0xFF if data_length > 0xFF
|
36
|
+
bytes = [0xFF, @sop2, @device_id, @command_id, sequence, data_length]
|
37
|
+
bytes.concat @data unless @data.nil?
|
38
|
+
|
39
|
+
sum = 0
|
40
|
+
bytes.each { |byte| sum = (sum + byte) }
|
41
|
+
bytes.push(((sum - 0xFF - @sop2) & 0xFF) ^ 0xFF)
|
42
|
+
bytes.pack('C*')
|
43
|
+
end
|
44
|
+
|
45
|
+
# The class used to parse the response for this command.
|
46
|
+
#
|
47
|
+
# Subclasses should override this method.
|
48
|
+
#
|
49
|
+
# @return {Class<SpheroPwn::Response>} the class that will be instantiated
|
50
|
+
# when this command's response is received
|
51
|
+
def response_class
|
52
|
+
SpheroPwn::Response
|
53
|
+
end
|
54
|
+
end
|