lego_ev3 0.9.0 → 0.9.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5b2fb90c9305d336e5d81ec1a7d7ac242542c1cc
4
- data.tar.gz: fcc6149efda2798a9e3c1cfff391812511cf9e14
3
+ metadata.gz: b418f6caad54717971ee644696289198bd6a1c62
4
+ data.tar.gz: cb47b39657e7913366bd821f76a35d6eabcd1d28
5
5
  SHA512:
6
- metadata.gz: 2eb2ba5f4c07fa9e387845ff5d1d27d89114294f94a4f2482456ba3142f4f25f23c5bb302e3dfa39b21ff0bf4a2e42c6447be2c32ab8e4a6689728e693d91f39
7
- data.tar.gz: 43e8cab0d3c77082c7ba1eab43c6f5d3299f9c939df5434f2aff32ba7976ee012101b018b49e6261350b9a4b825f37c048ca32c50873c61038762da40348769b
6
+ metadata.gz: 1b476defd2bde6d39c2d7bf843cdbba04f3dd4612a211deaa56537b6ee1458b059eda153ade4efc5b1b3151312ed2177bf7b5c42d3e067581e26c961c7446037
7
+ data.tar.gz: 5027148c8a6d338752fc3682ef0c8435dd3192da8a60c716ac305f93f276c1e939a343807b81f658d81a4a04bd24287b534ac8e54aa8a42febf1e8b38cb6e62b
data/README.md CHANGED
@@ -1,46 +1,129 @@
1
1
  # Lego EV3
2
2
 
3
+ This library leverages the [ev3dev.org](http://www.ev3dev.org) project to provide a dead simple way to program the Lego Mindstorms EV3 starter kit in Ruby.
4
+
3
5
  ## Getting started
4
6
 
5
- ### Make sure you have the necessary Ruby dependencies
7
+ gem install lego-ev3
8
+
9
+ Then:
10
+
11
+ mkdir example
12
+ touch example/script.rb
13
+ touch example/config.yml
14
+
15
+ Put in `script.rb`:
16
+
17
+ require 'lego_ev3'
18
+
19
+ connection = LegoEv3::Connection.new
20
+
21
+ brick = LegoEv3::Brick.new(connection)
22
+
23
+ # Plug any sensor in any input.
24
+ s = brick.sensors.first
25
+
26
+ 100.times do
27
+ puts s.poll
28
+ end
29
+
30
+ connection.close
31
+
32
+ Put in `config.yml`:
33
+
34
+ remote:
35
+ host: '192.168.2.3'
36
+ hostname: 'ev3dev'
37
+ username: 'root'
38
+ password: 'r00tme'
39
+ ssh: 22
40
+ tcp: 13603
41
+ service: 'ssh'
42
+
43
+ Execute the script with:
44
+
45
+ lego-ev3 example -R
46
+
47
+ This will open an SSH connection to the brick to send commands. This is one way of doing things, read below for more options.
48
+
49
+ ## Setup the brick to use Ruby
6
50
 
7
- The [ev3dev.org](http://www.ev3dev.org) distribution contains Ruby 2 but some libraries are missing to build native extensions. *This lib requires native extensions to work*.
51
+ ### Make sure you have the necessary dependencies
52
+
53
+ The [ev3dev.org](http://www.ev3dev.org) distribution contains Ruby 2 but some libraries are missing to build native extensions. **Running local scripts with this lib requires native extensions to work**.
8
54
 
9
55
  Solution #1: Use [RVM](https://rvm.io).
56
+
10
57
  Solution #2: Install those dependencies:
11
58
 
12
- ```
13
- apt-get install build-essential bison openssl libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libxml2-dev autoconf libc6-dev ncurses-dev automake libtool ruby-dev
14
- ```
59
+ apt-get install build-essential bison openssl libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libxml2-dev autoconf libc6-dev ncurses-dev automake libtool ruby-dev
60
+
61
+ ### (Optional) Faster `gem install` operations
62
+
63
+ To install gems faster, make sure your `~/.gemrc` on the brick looks like:
64
+
65
+ ---
66
+ :verbose: true
67
+ :sources:
68
+ - http://rubygems.org/
69
+ :update_sources: true
70
+ :backtrace: false
71
+ :bulk_threshold: 1000
72
+ :benchmark: false
73
+ gem: --no-ri --no-rdoc --verbose
74
+
75
+ ## Running a script remotely on the brick for debugging
76
+
77
+ This is the mode used in _Getting started_ above.
78
+ This mode requires `remote.service: 'ssh'` in the config.
79
+
80
+ * [+] No need to setup the brick to use Ruby
81
+ * [+] No need to send files to the brick
82
+ * [+] Can use breakpoints in script using [Pry](https://github.com/pry/pry)
83
+ * [-] The slowest approach (~200 ms per command)
84
+
85
+ ## Running a script locally on the brick
86
+
87
+ The same script in _Getting started_ above can be ran locally on the brick without modifying the code.
88
+
89
+ * [-] Need to setup the brick to use Ruby
90
+ * [-] Need to send files to the brick
91
+ * [-] Cannot use breakpoints in script
92
+ * [+] Fastest approach (~0.7 ms per command)
93
+
94
+ Sending a script to the brick:
95
+
96
+ lego-ev3 example -u -r
97
+
98
+ This will:
99
+
100
+ * Upload the script to the brick (`-u`) at `/home/example`.
101
+ * Execute `ruby /home/example/script.rb` through an SSH connection and capture the output.
102
+
103
+ **How can the same code be used remotely and locally?**
15
104
 
16
- ### Install the gem
105
+ The `LegoEv3::Connection` class is smart enough to determine if the script being executed is on your machine or directly on the brick. To accomplish this, this class uses the `hostname` parameter in the config.
17
106
 
18
- ```
19
- gem install lego-ev3
20
- ```
107
+ ## Running a script remotely on the brick for a test run
21
108
 
22
- ### A simple script
109
+ * [-] Need to setup the brick to use Ruby
110
+ * [+] No need to send files to the brick
111
+ * [+] Can use breakpoints in script using [Pry](https://github.com/pry/pry)
112
+ * [+] Slow approach (~40 ms per command)
23
113
 
24
- ```
25
- require 'lego_ev3'
114
+ This mode requires `remote.service: 'tcp'` in the config.
26
115
 
27
- # The connection class look at the hostname of the machine to determine
28
- # if the connection to establish must be local or remote (ssh).
29
- # machine.hostname != config.hostname => remote.
30
- connection = LegoEv3::Connection.new('ssh' => {
31
- 'host' => '192.168.2.3',
32
- 'hostname' => 'ev3dev',
33
- 'username' => 'root',
34
- 'password' => 'r00tme'
35
- })
116
+ lego-ev3 example -s # Spawn the TCP server on the brick
117
+ lego-ev3 example -R # Execute the script with a TCP client
36
118
 
37
- brick = LegoEv3::Brick.new(connection)
119
+ Note that the TCP server is spawned in background on the brick.
120
+ To spawn it in foreground, you need to connect to the brick.
38
121
 
39
- # Plug the touch sensor in any input and run this script by pressing
40
- # or not the sensor. The 'pressed' value should change accordingly.
41
- s = brick.sensors.first
42
- s.poll
43
- puts s.info.inspect
122
+ ## TODO
44
123
 
45
- connection.close
46
- ```
124
+ * Add a DSL on top of current lib
125
+ * Add a state machine on top of current lib
126
+ * Use the state machine to produce graphs of robot logic
127
+ * Add logging and easy way to produce sensor graphs
128
+ * Polling sensors should probably be multi-threaded
129
+ * Add examples
@@ -2,32 +2,85 @@
2
2
 
3
3
  require 'lego_ev3'
4
4
  require 'optparse'
5
+ require 'pp'
5
6
 
6
7
  options = {
7
- user_config: LegoEv3::default_user_config
8
+ upload: false,
9
+ mode: :none,
10
+ user_config: nil
8
11
  }
9
12
 
10
13
  OptionParser.new do |opt|
11
- opt.on('-u', '--upload PROJECT', 'Upload everything under ./.../PROJECT to /home/PROJECT on the brick.') do |project|
12
- options[:mode] = :upload
13
- options[:project] = project
14
+ opt.banner = 'Usage: lego-ev3 [project] [options]'
15
+
16
+ opt.on('-u', '--upload', 'Upload the project to /home/PROJECT on the brick.') do
17
+ options[:upload] = true
18
+ end
19
+
20
+ opt.on('-r', '--run', 'Run the project locally.') do
21
+ options[:mode] = :run_local
22
+ end
23
+
24
+ opt.on('-R', '--run-remotely', 'Run the project remotely.') do
25
+ options[:mode] = :run_remote
14
26
  end
15
27
 
16
- opt.on('-c, --config PATH', 'Use the provided configuration at PATH.') do |path|
28
+ opt.on('-c', '--config PATH', 'Use the provided configuration at PATH.') do |path|
17
29
  options[:user_config].merge!(LegoEv3::load_config(path))
18
30
  end
31
+
32
+ opt.on('-s', '--server', 'Start a TCP server on the brick.') do |port|
33
+ options[:mode] = :server
34
+ end
19
35
  end.parse!
20
36
 
37
+ options[:project] = ARGV.pop
38
+ options[:user_config] ||= options[:project].nil? || !File.exists?(File.join(options[:project], 'config.yml')) ?
39
+ LegoEv3::default_user_config :
40
+ LegoEv3::load_config(File.join(options[:project], 'config.yml'))
41
+
21
42
  puts
22
43
  puts "Config used:"
23
44
  puts
24
- ap options
45
+ pp options
25
46
  puts
26
47
 
27
- LegoEv3::Uploader
28
- .new(
29
- options[:user_config]['ssh']['host'],
30
- options[:user_config]['ssh']['username'],
31
- options[:user_config]['ssh']['password'],
32
- options[:project])
33
- .upload
48
+ if options[:project].nil? && options[:mode] != :server
49
+ puts 'This mode requires a project!'
50
+ puts 'Usage: lego-ev3 [project] [options]'
51
+ exit
52
+ end
53
+
54
+ ssh_config = {
55
+ host_name: options[:user_config]['remote']['host'],
56
+ port: options[:user_config]['remote']['ssh'],
57
+ user: options[:user_config]['remote']['username'],
58
+ password: options[:user_config]['remote']['password'],
59
+ timeout: 20
60
+ }
61
+
62
+ if options[:upload]
63
+ LegoEv3::Uploader
64
+ .new(ssh_config, options[:project])
65
+ .upload
66
+ end
67
+
68
+ if options[:mode] == :run_local
69
+ command =
70
+ "cd /home/#{options[:project].split('/').last} && " +
71
+ "ruby #{options[:user_config]['entry_point']}"
72
+
73
+ LegoEv3::SSHScript
74
+ .new(ssh_config, command)
75
+ .run
76
+
77
+ elsif options[:mode] == :run_remote
78
+ LegoEv3::user_config_overrides = options[:user_config]
79
+ load File.join(options[:project], options[:user_config]['entry_point'])
80
+ elsif options[:mode] == :server
81
+ command = "nohup lego-ev3-tcp-server -p #{options[:user_config]['remote']['tcp']} > /dev/null 2>&1 &"
82
+
83
+ LegoEv3::SSHScript
84
+ .new(ssh_config, command)
85
+ .run
86
+ end
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'lego_ev3'
4
+ require 'optparse'
5
+ require 'pp'
6
+
7
+ options = {
8
+ port: LegoEv3::default_user_config['remote']['tcp']
9
+ }
10
+
11
+ OptionParser.new do |opt|
12
+ opt.banner = 'Usage: lego-ev3-tcp-server [options]'
13
+
14
+ opt.on('-p', '--port PORT', 'The port to use') do |port|
15
+ options[:port] = port
16
+ end
17
+ end.parse!
18
+
19
+ LegoEv3::TCPServer.new(options[:port]).open
@@ -35,6 +35,8 @@ module LegoEv3
35
35
  end
36
36
  end
37
37
 
38
+ @connection.flush("Refreshed ports and their status")
39
+
38
40
  # Retrieve Port name -> Tacho motor in batch.
39
41
  tacho_motors = {}
40
42
  LegoEv3::Commands::TachoMotor.list!(@connection).each do |motor|
@@ -51,7 +53,7 @@ module LegoEv3
51
53
  end
52
54
  end
53
55
 
54
- @connection.flush
56
+ @connection.flush("Refreshed motors and sensors")
55
57
 
56
58
  # Assemble port info.
57
59
  @ports.each do |port|
@@ -79,7 +81,12 @@ module LegoEv3
79
81
 
80
82
  if driver_name == 'lego-ev3-touch'
81
83
  LegoEv3::TouchSensor.new(@connection, id, p)
84
+ elsif driver_name == 'lego-ev3-color'
85
+ LegoEv3::ColorSensor.new(@connection, id, p)
86
+ elsif driver_name == 'lego-ev3-ir'
87
+ LegoEv3::InfraredSensor.new(@connection, id, p)
82
88
  else
89
+ puts "The sensor #{driver_name} is not supported :( but I accept pull requests!"
83
90
  nil
84
91
  end
85
92
  end.compact
@@ -17,11 +17,11 @@ module LegoEv3
17
17
  end
18
18
  end
19
19
 
20
- def get(alias_name, type = String, command_name = nil, processor = nil)
20
+ def get(alias_name, type = String, command_name = nil, handle = 'r', processor = nil)
21
21
  command_name ||= alias_name
22
22
 
23
23
  self.define_singleton_method "get_#{alias_name}" do |connection, id, &callback|
24
- connection.send("cat #{get_path(id)}/#{command_name}") do |response|
24
+ connection.send(:read, "#{get_path(id)}/#{command_name}", nil, handle) do |response|
25
25
  sanitized = (response || '').strip
26
26
 
27
27
  if type == Integer
@@ -48,11 +48,11 @@ module LegoEv3
48
48
  end
49
49
  end
50
50
 
51
- def set(alias_name, command_name = nil)
51
+ def set(alias_name, command_name = nil, handle = 'r')
52
52
  command_name ||= alias_name
53
53
 
54
54
  self.define_singleton_method "set_#{alias_name}" do |connection, id, value|
55
- connection.send("echo #{value} > #{get_path(id)}/#{command_name}")
55
+ connection.send(:write, "#{get_path(id)}/#{command_name}", value.to_s, handle)
56
56
  end
57
57
 
58
58
  self.define_singleton_method "set_#{alias_name}!" do |connection, id, value|
@@ -62,17 +62,17 @@ module LegoEv3
62
62
  end
63
63
 
64
64
  def get_set(alias_name, type = String, command_name = nil, processor = nil)
65
- get(alias_name, type, command_name, processor)
66
- set(alias_name, command_name)
65
+ get(alias_name, type, command_name, 'r+', processor)
66
+ set(alias_name, command_name, 'r+')
67
67
  end
68
68
 
69
69
  def command(alias_name, command_name = nil)
70
70
  command_name ||= alias_name
71
71
 
72
72
  self.define_singleton_method alias_name do |connection, id|
73
- connection.send("echo #{command_name} > #{get_command_path(id)}")
73
+ connection.send(:write, get_command_path(id), command_name, 'w')
74
74
  end
75
-
75
+
76
76
  self.define_singleton_method "#{alias_name}!" do |connection, id|
77
77
  self.send(alias_name, connection, id)
78
78
  connection.flush
@@ -81,11 +81,8 @@ module LegoEv3
81
81
 
82
82
  def has_list
83
83
  self.define_singleton_method :list do |connection, &callback|
84
- connection.send("ls -C #{get_base_path}") do |response|
85
- # TODO: Bug? The folder is not created if no sensor plugged in once.
86
- entries_raw = response || ''
87
- entries = entries_raw.include?('No such file or directory') ? [] : entries_raw.split(' ').map(&:strip)
88
- callback.call(entries)
84
+ connection.send(:list, get_base_path, nil, 'r') do |response|
85
+ callback.call(response)
89
86
  end
90
87
  end
91
88
 
@@ -7,6 +7,8 @@ module LegoEv3::Commands::LegoSensor
7
7
  get :driver_name
8
8
  get :decimals, Integer
9
9
  get :num_values, Integer
10
+ get_set :mode
11
+ get :modes, String, 'modes', 'r', -> response { response.split(' ').map(&:strip) }
10
12
  get :value0, Integer
11
13
  get :value1, Integer
12
14
  get :value2, Integer
@@ -6,7 +6,7 @@ module LegoEv3::Commands::TachoMotor
6
6
  get :port_name
7
7
  get :count_per_rot, Integer
8
8
  get :duty_cycle, Integer
9
- get :states, String, 'state', -> response { response.split(' ').map{ |s| s.strip.to_sym } }
9
+ get :states, String, 'state', 'r', -> response { response.split(' ').map{ |s| s.strip.to_sym } }
10
10
  get_set :duty_cycle_sp, Integer
11
11
  get_set :position, Integer
12
12
  get_set :position_sp, Integer
@@ -4,28 +4,25 @@ module LegoEv3
4
4
  @to_send = []
5
5
  end
6
6
 
7
- def send(command, &callback)
8
- @to_send << [command, callback]
7
+ def send(verb, path, value = nil, handle = 'r+', &callback)
8
+ @to_send << [[verb, path, value, handle], callback]
9
9
  end
10
10
 
11
- def flush
11
+ def flush(summary = nil)
12
12
  @connection ||= create_connection
13
13
 
14
- joined_command = @to_send
15
- .map{ |(c, _)| c }
16
- .join(';')
14
+ commands = @to_send.map{ |(c, _)| c }
15
+ callbacks = @to_send.map{ |(_, c)| c }
17
16
 
18
- callbacks = @to_send
19
- .map{ |(_, c)| c }
20
-
21
- joined_response, time = LegoEv3::with_timer do
22
- call_connection(joined_command)
17
+ responses, time = LegoEv3::with_timer do
18
+ call_connection(commands)
23
19
  end
24
20
 
25
- puts "#{joined_command}. #{time} ms."
21
+ summary ||= commands
22
+ .map{ |c| "[#{c.join(' ')}]" }
23
+ .join(' ')
26
24
 
27
- # We assume that one command output one line of result.
28
- responses = joined_response.split("\n")
25
+ puts "#{summary}. #{time} ms."
29
26
 
30
27
  callbacks.each_with_index.each do |c, i|
31
28
  c.call(responses[i]) if c
@@ -1,36 +1,34 @@
1
- require 'optparse'
2
-
3
1
  module LegoEv3
4
2
  class Connection
5
3
  def initialize(user_config = {})
6
4
  options = {
7
- user_config: LegoEv3::default_user_config.merge(user_config)
5
+ user_config: LegoEv3::resolve_user_config
8
6
  }
9
7
 
10
- OptionParser.new do |opt|
11
- opt.on('-c, --config PATH', 'Use the provided configuration at PATH.') do |path|
12
- options[:user_config].merge!(LegoEv3::load_config(path))
13
- end
14
- end.parse!
15
- is_local = `hostname`.strip == options[:user_config]['ssh']['hostname']
8
+ is_local = `hostname`.strip == options[:user_config]['remote']['hostname']
16
9
 
17
10
  @inner_connection = if is_local
18
11
  LegoEv3::LocalConnection.new
19
- else
20
- LegoEv3::RemoteConnection.new(
21
- options[:user_config]['ssh']['host'],
22
- options[:user_config]['ssh']['username'],
23
- options[:user_config]['ssh']['password']
12
+ elsif options[:user_config]['remote']['service'].to_sym == :tcp
13
+ LegoEv3::TCPConnection.new(options[:user_config]['remote']['host'] + ':' + options[:user_config]['remote']['tcp'].to_s)
14
+ elsif options[:user_config]['remote']['service'].to_sym == :ssh
15
+ LegoEv3::SSHConnection.new(
16
+ options[:user_config]['remote']['host'],
17
+ options[:user_config]['remote']['ssh'],
18
+ options[:user_config]['remote']['username'],
19
+ options[:user_config]['remote']['password']
24
20
  )
21
+ else
22
+ raise LegoEv3::InvalidRemoteServiceException.new(options[:user_config]['remote']['service'])
25
23
  end
26
24
  end
27
25
 
28
- def send(command, &callback)
29
- @inner_connection.send(command, &callback)
26
+ def send(verb, path, value = nil, handle = 'r+', &callback)
27
+ @inner_connection.send(verb, path, value, handle, &callback)
30
28
  end
31
29
 
32
- def flush
33
- @inner_connection.flush
30
+ def flush(summary = nil)
31
+ @inner_connection.flush(summary)
34
32
  end
35
33
 
36
34
  def close