lego_ev3 0.9.0 → 0.9.1

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