sphero_pwn 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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