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 +4 -4
- data/README.md +112 -29
- data/bin/lego-ev3 +66 -13
- data/bin/lego-ev3-tcp-server +19 -0
- data/lib/brick.rb +8 -1
- data/lib/commands/builder.rb +10 -13
- data/lib/commands/lego_sensor.rb +2 -0
- data/lib/commands/tacho_motor.rb +1 -1
- data/lib/connection/base.rb +11 -14
- data/lib/connection/connection.rb +16 -18
- data/lib/connection/local.rb +48 -3
- data/lib/connection/ssh.rb +84 -0
- data/lib/connection/ssh_command.rb +44 -0
- data/lib/connection/tcp_client.rb +64 -0
- data/lib/connection/tcp_server.rb +63 -0
- data/lib/connection/upload.rb +34 -24
- data/lib/exceptions.rb +35 -1
- data/lib/lego_ev3.rb +7 -0
- data/lib/sensors/base.rb +19 -12
- data/lib/sensors/color.rb +76 -0
- data/lib/sensors/infrared.rb +142 -0
- data/lib/sensors/touch.rb +1 -2
- data/lib/tacho_motor.rb +49 -28
- data/lib/utilities.rb +38 -2
- metadata +10 -3
- data/lib/connection/remote.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b418f6caad54717971ee644696289198bd6a1c62
|
4
|
+
data.tar.gz: cb47b39657e7913366bd821f76a35d6eabcd1d28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/bin/lego-ev3
CHANGED
@@ -2,32 +2,85 @@
|
|
2
2
|
|
3
3
|
require 'lego_ev3'
|
4
4
|
require 'optparse'
|
5
|
+
require 'pp'
|
5
6
|
|
6
7
|
options = {
|
7
|
-
|
8
|
+
upload: false,
|
9
|
+
mode: :none,
|
10
|
+
user_config: nil
|
8
11
|
}
|
9
12
|
|
10
13
|
OptionParser.new do |opt|
|
11
|
-
opt.
|
12
|
-
|
13
|
-
|
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
|
-
|
45
|
+
pp options
|
25
46
|
puts
|
26
47
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
data/lib/brick.rb
CHANGED
@@ -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
|
data/lib/commands/builder.rb
CHANGED
@@ -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("
|
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("
|
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(
|
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(
|
85
|
-
|
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
|
|
data/lib/commands/lego_sensor.rb
CHANGED
@@ -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
|
data/lib/commands/tacho_motor.rb
CHANGED
@@ -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
|
data/lib/connection/base.rb
CHANGED
@@ -4,28 +4,25 @@ module LegoEv3
|
|
4
4
|
@to_send = []
|
5
5
|
end
|
6
6
|
|
7
|
-
def send(
|
8
|
-
@to_send << [
|
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
|
-
|
15
|
-
|
16
|
-
.join(';')
|
14
|
+
commands = @to_send.map{ |(c, _)| c }
|
15
|
+
callbacks = @to_send.map{ |(_, c)| c }
|
17
16
|
|
18
|
-
|
19
|
-
|
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
|
-
|
21
|
+
summary ||= commands
|
22
|
+
.map{ |c| "[#{c.join(' ')}]" }
|
23
|
+
.join(' ')
|
26
24
|
|
27
|
-
#
|
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::
|
5
|
+
user_config: LegoEv3::resolve_user_config
|
8
6
|
}
|
9
7
|
|
10
|
-
|
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
|
-
|
20
|
-
LegoEv3::
|
21
|
-
|
22
|
-
|
23
|
-
options[:user_config]['
|
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(
|
29
|
-
@inner_connection.send(
|
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
|