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.
@@ -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
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.2.3
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
@@ -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
@@ -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.
@@ -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.
@@ -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
@@ -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,10 @@
1
+ # Superclass for all asynchronous messages sent from the robot to the computer.
2
+ class SpheroPwn::Async
3
+ # @return {Array<Number>} the payload
4
+ attr_reader :data_bytes
5
+
6
+
7
+ def initialize(data_bytes)
8
+ @data_bytes = data_bytes
9
+ end
10
+ end
@@ -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