rubotz 0.0.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.
- data/README +95 -0
- data/config/nxt_home.yaml +1 -0
- data/examples/table_wanderer.rb +66 -0
- data/lib/rubotz.rb +58 -0
- data/lib/rubotz/generator.rb +103 -0
- data/lib/rubotz/motor_generator.rb +49 -0
- data/lib/rubotz/ultrasonic_sensor.rb +57 -0
- data/rubotz-0.0.1.gem +0 -0
- data/rubotz.gemspec +14 -0
- data/slow_spec/compiler_spec.rb +24 -0
- data/spec/generating_spec.rb +17 -0
- data/spec/table_wanderer_spec.rb +211 -0
- data/templates/simple_while.nxc.erb +8 -0
- metadata +72 -0
data/README
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
= Rubotz
|
2
|
+
|
3
|
+
Rubotz is a Ruby library which generates and compiles NXC programs for Lego Mindstorms
|
4
|
+
NXT robots.
|
5
|
+
|
6
|
+
You can program your robot in Ruby, including the compilation phase, and then simply
|
7
|
+
copy the generated executable from your computer to your Mindstorms robot with third-party
|
8
|
+
programs such as NXTBrowser.
|
9
|
+
|
10
|
+
= Proof-Of-Concept Release Only
|
11
|
+
|
12
|
+
Rubotz v0.0.1 is a proof-of-concept release. It implements only a subset of available motor
|
13
|
+
functionality and only one sensor (the ultrasonic sensor). Mindstorms ships with 4 sensors
|
14
|
+
and additional third-party sensors are available as well. NXC supports a multi-threaded
|
15
|
+
model, which is very cool; Rubotz currently only supports one ("main") thread.
|
16
|
+
|
17
|
+
Support for multiple threads is planned; support for all stock sensors and the full
|
18
|
+
range of motor functionality is planned as well.
|
19
|
+
|
20
|
+
= Documentation Is For Computers To Read
|
21
|
+
|
22
|
+
I don't really believe in documentation; I believe that your documentation should consist
|
23
|
+
entirely of your specs. Specs are documentation which you can run as code, and therefore
|
24
|
+
they are *self-verifying* documentation; any other kind of documentation is really just a
|
25
|
+
statement of opinion. However, I definitely want people who are interested to be able to
|
26
|
+
use the software!
|
27
|
+
|
28
|
+
The specs dir absolutely contains the best documentation, but I've also made an effort to
|
29
|
+
augment that with text and RDoc. However, if you're confused about something, the specs are
|
30
|
+
*by definition* the canonical example of how to do it.
|
31
|
+
|
32
|
+
== How To Program Your Robot In Ruby
|
33
|
+
|
34
|
+
Rubotz.compile takes an options hash, using it as faux keyword arguments in the Rails style.
|
35
|
+
Rubotz.program takes a block, with Ruby code which it then translates into NXC.
|
36
|
+
|
37
|
+
The examples directory contains an example as runnable code - logically enough - but I'm
|
38
|
+
repeating it here to demonstrate.
|
39
|
+
|
40
|
+
This code is based on a robot I built which aims an ultrasonic sensor at the floor and drives
|
41
|
+
forward, backing away and turning if it encounters a dropoff indicating a table edge.
|
42
|
+
|
43
|
+
Check it out on YouTube: http://www.youtube.com/watch?v=uQ4BwxH7uoY
|
44
|
+
(it's pretty straightforward).
|
45
|
+
|
46
|
+
Rubotz.compile :file => "rubotz",
|
47
|
+
:program => (Rubotz.program do
|
48
|
+
the_robot "drives around the table scanning its ultrasonic sensor"
|
49
|
+
initializing do
|
50
|
+
short ultrasonic_sensor
|
51
|
+
activate(UltrasonicSensor.port(4))
|
52
|
+
end
|
53
|
+
looping do
|
54
|
+
clear_screen
|
55
|
+
monitoring(UltrasonicSensor) do |ultrasonic_sensor|
|
56
|
+
ultrasonic_sensor.> 30 do
|
57
|
+
display :line => 7, :text => "Danger"
|
58
|
+
display :line => 8, :number => ultrasonic_sensor
|
59
|
+
sound "! Attention.rso"
|
60
|
+
motor :synchronized => true,
|
61
|
+
:direction => :reverse,
|
62
|
+
:ports => [:B, :C],
|
63
|
+
:power => 25,
|
64
|
+
:turn => 0
|
65
|
+
wait 1500
|
66
|
+
motor :synchronized => true,
|
67
|
+
:direction => :reverse,
|
68
|
+
:ports => [:B, :C],
|
69
|
+
:power => 25,
|
70
|
+
:turn => 100
|
71
|
+
wait 325
|
72
|
+
end
|
73
|
+
ultrasonic_sensor.<= 30 do
|
74
|
+
display :line => 7, :text => "Safe"
|
75
|
+
display :line => 8, :number => ultrasonic_sensor
|
76
|
+
motor :synchronized => true,
|
77
|
+
:direction => :forwards,
|
78
|
+
:ports => [:B, :C],
|
79
|
+
:power => 35,
|
80
|
+
:turn => 5
|
81
|
+
end
|
82
|
+
end
|
83
|
+
wait 25
|
84
|
+
end
|
85
|
+
end)
|
86
|
+
|
87
|
+
The above code successfully generates NXC code for the table robot, and successfully
|
88
|
+
compiles that code using my NXC install into a format which successfully runs on my Mindstorms
|
89
|
+
robot brain. To transfer the generated code to the robot brain, I use NXTBrowser on OS X:
|
90
|
+
|
91
|
+
http://web.mac.com/carstenm/Lego/NXT/Entries/2006/12/4_NXTBrowser_-_Bluetooth_connectivity_on_an_Intel_Mac.html
|
92
|
+
|
93
|
+
I'm very confident that alternatives exist for other platforms. I'm also very confident this will
|
94
|
+
work on other people's computers and Mindstorms brains as well as my own, but definitely correct me
|
95
|
+
if I got that wrong! (You can reach me by e-mail at gilesb@gmail.com.)
|
@@ -0,0 +1 @@
|
|
1
|
+
nxt_home: /Users/giles/programming/nxt
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# If you don't know what you're doing here, please be careful, as you **are** using this software
|
2
|
+
# at your own risk. Some Mindstorms users are kids; while a kid could figure this out, you would
|
3
|
+
# have to be careful to use NXTBrowser correctly, and be careful with Ruby too. Using this software
|
4
|
+
# at your own risk means that if your Mindstorms is destroyed, I won't buy you a new one, and that
|
5
|
+
# goes double for your computer.
|
6
|
+
|
7
|
+
# This code is based on a robot I built which aims an ultrasonic sensor at the floor and drives
|
8
|
+
# forward, backing away and turning if it encounters a dropoff indicating a table edge.
|
9
|
+
|
10
|
+
# Check it out on YouTube: http://www.youtube.com/watch?v=uQ4BwxH7uoY
|
11
|
+
# (it's pretty straightforward).
|
12
|
+
|
13
|
+
# If you build that robot, or an equivalent robot, and you have an appropriate config file in
|
14
|
+
# the /config dir - replacing the default, which points to a subdir of my own home dir - you
|
15
|
+
# should be able to auto-generate an NXC program simply by running the Ruby code in this file.
|
16
|
+
# You can then transfer the .rxe generated executable to your Mindstorms NXT robot using
|
17
|
+
# a third-party app like (for instance) NXTBrowser:
|
18
|
+
# http://web.mac.com/carstenm/Lego/NXT/Entries/2006/12/4_NXTBrowser_-_Bluetooth_connectivity_on_an_Intel_Mac.html
|
19
|
+
|
20
|
+
# I used this program to drive development of Rubotz v0.0.1, so check out the table wanderer
|
21
|
+
# spec for a lot more detail on how to use Rubotz. You will need to have NXC installed on your
|
22
|
+
# system, with a working compiler.
|
23
|
+
|
24
|
+
require File.join(File.dirname(__FILE__), '../lib/rubotz')
|
25
|
+
include Rubotz
|
26
|
+
|
27
|
+
Rubotz.compile :file => "rubotz",
|
28
|
+
:program => (Rubotz.program do
|
29
|
+
the_robot "drives around the table scanning its ultrasonic sensor"
|
30
|
+
initializing do
|
31
|
+
short ultrasonic_sensor
|
32
|
+
activate(UltrasonicSensor.port(4))
|
33
|
+
end
|
34
|
+
looping do
|
35
|
+
clear_screen
|
36
|
+
monitoring(UltrasonicSensor) do |ultrasonic_sensor|
|
37
|
+
ultrasonic_sensor.> 30 do
|
38
|
+
display :line => 7, :text => "Danger"
|
39
|
+
display :line => 8, :number => ultrasonic_sensor
|
40
|
+
sound "! Attention.rso"
|
41
|
+
motor :synchronized => true,
|
42
|
+
:direction => :reverse,
|
43
|
+
:ports => [:B, :C],
|
44
|
+
:power => 25,
|
45
|
+
:turn => 0
|
46
|
+
wait 1500
|
47
|
+
motor :synchronized => true,
|
48
|
+
:direction => :reverse,
|
49
|
+
:ports => [:B, :C],
|
50
|
+
:power => 25,
|
51
|
+
:turn => 100
|
52
|
+
wait 325
|
53
|
+
end
|
54
|
+
ultrasonic_sensor.<= 30 do
|
55
|
+
display :line => 7, :text => "Safe"
|
56
|
+
display :line => 8, :number => ultrasonic_sensor
|
57
|
+
motor :synchronized => true,
|
58
|
+
:direction => :forwards,
|
59
|
+
:ports => [:B, :C],
|
60
|
+
:power => 35,
|
61
|
+
:turn => 5
|
62
|
+
end
|
63
|
+
end
|
64
|
+
wait 25
|
65
|
+
end
|
66
|
+
end)
|
data/lib/rubotz.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
%w{erb
|
2
|
+
yaml
|
3
|
+
lib/rubotz/generator
|
4
|
+
lib/rubotz/ultrasonic_sensor}.each {|lib| require lib}
|
5
|
+
|
6
|
+
module Rubotz
|
7
|
+
SIMPLE_WHILE = "#{File.dirname(__FILE__)}/../templates/simple_while.nxc.erb"
|
8
|
+
CONFIG = YAML.load_file("#{File.dirname(__FILE__)}/../config/nxt_home.yaml")
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Rubotz.compile takes two keyword arguments - a filename and a Rubotz.program
|
12
|
+
# call, which takes a block. Here's the simplest possible instance:
|
13
|
+
#
|
14
|
+
# Rubotz.compile(:filename => "f", :program => (Rubotz.program {true}))
|
15
|
+
#
|
16
|
+
# Technically even simpler instances are *possible*, but would probably do bad
|
17
|
+
# things.
|
18
|
+
#
|
19
|
+
# One bad thing to watch out for is that there is a hard limit on the number of
|
20
|
+
# characters you can use in a filename, and I'm not yet sure what that is. However,
|
21
|
+
# beyond a certain number of characters, Mindstorms won't recognize the .rxe
|
22
|
+
# suffix on your generated executable, and when that happens, it assumes the file
|
23
|
+
# is data rather than a program. The workaround is obvious: use short file names.
|
24
|
+
#
|
25
|
+
# One other caveat: compile saves your generated code, and will overwrite existing
|
26
|
+
# generated code if it was generated with the same :filename. Keep an eye on your
|
27
|
+
# nxt/code dir, or use unique filenames, or be bold and fearless. Whatever. It's up
|
28
|
+
# to you.
|
29
|
+
def compile(options = {})
|
30
|
+
filename = options[:file].gsub(/ /, "_")
|
31
|
+
File.open("#{Rubotz::CONFIG['nxt_home']}/code/#{filename}.nxc", "w") do |file|
|
32
|
+
file.puts options[:program]
|
33
|
+
end
|
34
|
+
system ("cd #{Rubotz::CONFIG['nxt_home']}; " +
|
35
|
+
"./nbc " +
|
36
|
+
"-O=bin/#{filename}.rxe " +
|
37
|
+
"code/#{filename}.nxc")
|
38
|
+
end
|
39
|
+
# Rubotz.generate is an internal method. It's not marked private because I don't
|
40
|
+
# really care, I kind of have a laissez-faire, Perlish attitude to methods, if you
|
41
|
+
# shoot yourself in the foot, hey, it's your foot. However, it's really only useful
|
42
|
+
# if you're writing new generator code.
|
43
|
+
def generate(&block)
|
44
|
+
Generator.new(&block).code
|
45
|
+
end
|
46
|
+
# Rubotz.program takes a block of Ruby code and evals it in the context of a Generator,
|
47
|
+
# which knows how to turn that code into NXC code. It then runs the generated NXC code
|
48
|
+
# through ERb, to mesh with existing boilerplate stuff in NXC. Note however that it does
|
49
|
+
# not save a file, it simply returns the generated code in the ERb template as text.
|
50
|
+
def program(&block)
|
51
|
+
generator = Generator.new(&block)
|
52
|
+
initialization_section = generator.initialization_section
|
53
|
+
loop_body = generator.loop_body
|
54
|
+
comment = generator.comment || ""
|
55
|
+
ERB.new(File.read(SIMPLE_WHILE)).result(binding)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'lib/rubotz/motor_generator'
|
2
|
+
module Rubotz
|
3
|
+
# Pretty much all the methods that you program Rubotz robots with live in this class. (It
|
4
|
+
# may be wiser to isolate those methods in an API module and then mix that in, since that
|
5
|
+
# would make the documentation clearer, but this is how it works for now.)
|
6
|
+
#
|
7
|
+
# Instantiating an object of this class should never be necessary unless you're rewriting
|
8
|
+
# the main module or doing similar development. In the course of actually programming a robot,
|
9
|
+
# you just use the methods of the class as if they were regular Ruby keywords.
|
10
|
+
class Generator
|
11
|
+
attr_accessor :code,
|
12
|
+
:initialization_section,
|
13
|
+
:loop_body,
|
14
|
+
:comment
|
15
|
+
def initialize(&block)
|
16
|
+
@code = ""
|
17
|
+
instance_eval(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Example: activate(UltrasonicSensor.port(4)). You need to do this in the initialization
|
21
|
+
# section of your program, before going into the loop body and actually accessing the sensor.
|
22
|
+
def activate(sensor)
|
23
|
+
@code += sensor
|
24
|
+
end
|
25
|
+
# Does what it says on the tin.
|
26
|
+
def clear_screen
|
27
|
+
@code += "ClearScreen();\n"
|
28
|
+
end
|
29
|
+
# The display method takes a hash of symbols and strings, and uses that to write to the
|
30
|
+
# Mindstorms NXT display. For example:
|
31
|
+
#
|
32
|
+
# display :line => 7, :text => "Hello World"
|
33
|
+
# display :line => 8, :number => ultrasonic_sensor
|
34
|
+
#
|
35
|
+
# (Although support for variable interpolation is currently imperfect, it works perfectly
|
36
|
+
# for this particular method.)
|
37
|
+
#
|
38
|
+
# The NXT brain has 8 lines on its display, and you probably have to call clear_screen
|
39
|
+
# before you update it. Numbering starts at 1, not 0.
|
40
|
+
def display(options = {})
|
41
|
+
line_number = options[:line] || 1
|
42
|
+
if options.include?(:text)
|
43
|
+
@code += "TextOut(0, LCD_LINE#{line_number}, \"#{options[:text]}\");\n"
|
44
|
+
elsif options.include?(:number)
|
45
|
+
@code += "NumOut(0, LCD_LINE#{line_number}, #{options[:number]});\n"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
# The current Rubotz model is very limited; you have one constantly-running loop, and an
|
49
|
+
# initialization section beforehand, which is where you declare variables and activate sensors.
|
50
|
+
# Use initializing to wrap code which goes in the initialization section of a loop, rather
|
51
|
+
# than in the loop itself.
|
52
|
+
def initializing(&block)
|
53
|
+
@initialization_section = Rubotz.generate(&block)
|
54
|
+
end
|
55
|
+
# The current Rubotz model is very limited; you have one constantly-running loop, and an
|
56
|
+
# initialization section beforehand, which is where you declare variables and activate sensors.
|
57
|
+
# Use looping to wrap code which goes in the constantly-running loop.
|
58
|
+
def looping(&block)
|
59
|
+
@loop_body = Rubotz.generate(&block)
|
60
|
+
end
|
61
|
+
# This method_missing assumes missing methods are actually references to as-yet-undefined
|
62
|
+
# variables. Although variable interpolation is currently imperfect in Rubotz, this method
|
63
|
+
# causes all its successes.
|
64
|
+
def method_missing(method, *args)
|
65
|
+
"#{method}"
|
66
|
+
end
|
67
|
+
# Use monitoring to wrap blocks which check a sensor for data and then make decisions based
|
68
|
+
# on that data. Its args are a sensor class, and the block you're wrapping in it.
|
69
|
+
def monitoring(sensor_class, &block)
|
70
|
+
@code += sensor_class.new.instance_eval(&block)
|
71
|
+
end
|
72
|
+
# The motor method simply passes a MotorGenerator the options you specify; see the MotorGenerator
|
73
|
+
# RDoc for details on how to use this method, and what those options should be.
|
74
|
+
def motor(options = {})
|
75
|
+
@code += MotorGenerator.new(options).generate
|
76
|
+
end
|
77
|
+
# The short method allows you to declare a variable of the short type.
|
78
|
+
def short(variable_name)
|
79
|
+
@code += "short #{variable_name};\n"
|
80
|
+
end
|
81
|
+
# This method plays a sound which already exists on your NXC. Third-party utilities such as
|
82
|
+
# NXTBrowser make transferring new sounds to your Mindstorms NXT a piece of cake.
|
83
|
+
def sound(filename)
|
84
|
+
@code += "PlayFile(\"#{filename}\");\n"
|
85
|
+
end
|
86
|
+
# This method merely generates a comment in the generated NXC source code.
|
87
|
+
def the_robot(does_something)
|
88
|
+
@comment = "// the robot #{does_something}\n"
|
89
|
+
end
|
90
|
+
# This method pauses program execution. It doesn't cause the _robot_ to wait, but the
|
91
|
+
# _program_. This makes it a vital method. After you turn your motors on and start moving
|
92
|
+
# in a given direction, you need to wait an arbitrary amount of time before you do anything
|
93
|
+
# else; otherwise you'll simply turn on your motors and the program will immediately jump
|
94
|
+
# to the next instruction without actually driving anywhere.
|
95
|
+
#
|
96
|
+
# I think wait works in terms of milliseconds, but honestly I don't know. The units are some
|
97
|
+
# kind of fraction of a second which appears to work in some order of magnitude. In practical
|
98
|
+
# terms, just use round numbers and experiment to see what works for you.
|
99
|
+
def wait(number)
|
100
|
+
@code += "Wait(#{number});\n"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Rubotz
|
2
|
+
# The MotorGenerator initializes with an options hash instead of a block. For example:
|
3
|
+
#
|
4
|
+
# motor :synchronized => true,
|
5
|
+
# :direction => :reverse,
|
6
|
+
# :ports => [:B, :C],
|
7
|
+
# :power => 25,
|
8
|
+
# :turn => 0
|
9
|
+
#
|
10
|
+
# The NXC API has a number of different options for motors. This seemed like the best way
|
11
|
+
# to implement them, but currently MotorGenerator implements only a subset of available
|
12
|
+
# motor functionality, and if/when that functionality expands, the structure of the class
|
13
|
+
# may change.
|
14
|
+
#
|
15
|
+
# Currently MotorGenerators can only generate Sync commands, and only know how to power
|
16
|
+
# ports B and C.
|
17
|
+
#
|
18
|
+
# The methods in this class are basically irrelevant from a user perspective. What matters
|
19
|
+
# is the keywords you use, or more precisely the symbol keys to the options hash. First the
|
20
|
+
# lame stuff: :synchronized makes no difference at all, and the only legal values for :ports
|
21
|
+
# is [:B, :C], and you have to specify that. Sorry.
|
22
|
+
#
|
23
|
+
# However, :direction, :power, and :turn are all actually valid and usable. They're all also
|
24
|
+
# reasonably obvious. :direction is either :reverse or :forwards. I'm pretty sure :power maxes
|
25
|
+
# out at 100, and I know it starts at 0. :turn also starts at 0 but honestly I have no idea
|
26
|
+
# what its legal maximum is, if any. Feel free to experiment with this!
|
27
|
+
class MotorGenerator
|
28
|
+
def initialize(options = {})
|
29
|
+
@options = options
|
30
|
+
end
|
31
|
+
def generate
|
32
|
+
"#{motor_command}(#{ports}, #{power_and_turn});\n"
|
33
|
+
end
|
34
|
+
def motor_command
|
35
|
+
if :reverse == @options[:direction] # && @options[:synchronized]
|
36
|
+
"OnRevSync"
|
37
|
+
elsif :forwards == @options[:direction] # && @options[:synchronized]
|
38
|
+
"OnFwdSync"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
def power_and_turn
|
42
|
+
"#{@options[:power] || 0}, #{@options[:turn] || 0}"
|
43
|
+
end
|
44
|
+
def ports
|
45
|
+
"OUT_BC" if [:B, :C] == @options[:ports]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Rubotz
|
2
|
+
# The UltrasonicSensor class is a code generator for the NXT ultrasonic sensor. It provides
|
3
|
+
# simple value comparison methods, value assignment NXC code, and an activation class method.
|
4
|
+
# Its variable is currently hard-coded; this is planned to change.
|
5
|
+
#
|
6
|
+
# Ultimately all XyzSensors will inherit from a base Sensor class. This class will very likely be
|
7
|
+
# the only one to override Sensor's #port method (see NXC docs for why).
|
8
|
+
class UltrasonicSensor
|
9
|
+
# Create the UltrasonicSensor, with a variable declaration and relevant NXC API code.
|
10
|
+
def initialize
|
11
|
+
@code = "ultrasonic_sensor = SensorUS(S4);\n"
|
12
|
+
end
|
13
|
+
class << self
|
14
|
+
# Activation code for initialization blocks.
|
15
|
+
def port(number)
|
16
|
+
"SetSensorLowspeed(S#{number});\n"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Utility method for variable interpolation in code which refers to this sensor.
|
21
|
+
def to_s
|
22
|
+
"ultrasonic_sensor"
|
23
|
+
end
|
24
|
+
# UltrasonicSensor#> generates an if block around the >, the sensor itself (as an NXC
|
25
|
+
# variable), and the value to be compared against. It then executes arbitrary Ruby within
|
26
|
+
# the context of that if block. In practice this means turning Rubotz code into NXC code.
|
27
|
+
# (The instance method approach to value comparison is definitely a bit of a kludge, but it
|
28
|
+
# was a lot less work for me personally than implementing a parser.)
|
29
|
+
def > (number, &block)
|
30
|
+
@code += "if (ultrasonic_sensor > #{number}) {\n" + Rubotz.generate(&block) + "}\n"
|
31
|
+
end
|
32
|
+
# UltrasonicSensor#>= generates an if block around the >=, the sensor itself (as an NXC
|
33
|
+
# variable), and the value to be compared against. It then executes arbitrary Ruby within
|
34
|
+
# the context of that if block. In practice this means turning Rubotz code into NXC code.
|
35
|
+
# (The instance method approach to value comparison is definitely a bit of a kludge, but it
|
36
|
+
# was a lot less work for me personally than implementing a parser.)
|
37
|
+
def >= (number, &block)
|
38
|
+
@code += "if (ultrasonic_sensor >= #{number}) {\n" + Rubotz.generate(&block) + "}\n"
|
39
|
+
end
|
40
|
+
# UltrasonicSensor#< generates an if block around the <, the sensor itself (as an NXC
|
41
|
+
# variable), and the value to be compared against. It then executes arbitrary Ruby within
|
42
|
+
# the context of that if block. In practice this means turning Rubotz code into NXC code.
|
43
|
+
# (The instance method approach to value comparison is definitely a bit of a kludge, but it
|
44
|
+
# was a lot less work for me personally than implementing a parser.)
|
45
|
+
def < (number, &block)
|
46
|
+
@code += "if (ultrasonic_sensor < #{number}) {\n" + Rubotz.generate(&block) + "}\n"
|
47
|
+
end
|
48
|
+
# UltrasonicSensor#<= generates an if block around the <=, the sensor itself (as an NXC
|
49
|
+
# variable), and the value to be compared against. It then executes arbitrary Ruby within
|
50
|
+
# the context of that if block. In practice this means turning Rubotz code into NXC code.
|
51
|
+
# (The instance method approach to value comparison is definitely a bit of a kludge, but it
|
52
|
+
# was a lot less work for me personally than implementing a parser.)
|
53
|
+
def <= (number, &block)
|
54
|
+
@code += "if (ultrasonic_sensor <= #{number}) {\n" + Rubotz.generate(&block) + "}\n"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/rubotz-0.0.1.gem
ADDED
File without changes
|
data/rubotz.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
SPEC = Gem::Specification.new do |s|
|
3
|
+
s.name = s.rubyforge_project = "rubotz"
|
4
|
+
s.version = "0.0.1"
|
5
|
+
s.author = "Giles Bowkett"
|
6
|
+
s.email = "gilesb@gmail.com"
|
7
|
+
s.homepage = "http://rubotz.rubyforge.org"
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.summary = "Code generator for Lego Mindstorms NXT. Proof-of-concept release only."
|
10
|
+
s.files = Dir.glob("**/*")
|
11
|
+
s.require_path = "lib"
|
12
|
+
s.has_rdoc = true
|
13
|
+
s.extra_rdoc_files = ["README"]
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# this spec is painfully slow. everything else runs fast, but this one example can take a
|
2
|
+
# second and a half. I'm breaking BDD rules here with such a slow, unmocked spec, but it was
|
3
|
+
# useful for me to make sure the compiler stuff really worked. this spec lives in slow_spec
|
4
|
+
# rather than spec so that you can do the usual "spec spec" and get the majority of specs
|
5
|
+
# taken care of super-quick.
|
6
|
+
|
7
|
+
require "lib/rubotz"
|
8
|
+
|
9
|
+
describe Rubotz, "compiling an empty program" do
|
10
|
+
|
11
|
+
before(:each) do
|
12
|
+
@blank_defaults = {:program => (Rubotz.program do ; end),
|
13
|
+
:file => "simple program"}
|
14
|
+
end
|
15
|
+
|
16
|
+
it "puts output in the output dir" do
|
17
|
+
Rubotz.compile(@blank_defaults)
|
18
|
+
File.exists?("#{Rubotz::CONFIG["nxt_home"]}/bin/simple_program.rxe").should be_true
|
19
|
+
end
|
20
|
+
|
21
|
+
after(:all) do
|
22
|
+
system("cd #{Rubotz::CONFIG["nxt_home"]}; rm code/simple_program.nxc bin/simple_program.rxe")
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "lib/rubotz"
|
2
|
+
|
3
|
+
describe Rubotz, "generating an empty program" do
|
4
|
+
|
5
|
+
it "generates blank programs in uncompiled NXC, not counting indentation" do
|
6
|
+
simple_while =<<SIMPLE_WHILE
|
7
|
+
#include "NXCDefs.h"
|
8
|
+
task main() {
|
9
|
+
|
10
|
+
while(true) {
|
11
|
+
|
12
|
+
}
|
13
|
+
}
|
14
|
+
SIMPLE_WHILE
|
15
|
+
(Rubotz.program do ; end).gsub(/\s+/, " ").should == simple_while.gsub(/\s+/, " ")
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# This code is based on a robot I built which aims an ultrasonic sensor at the floor and drives
|
2
|
+
# forward, backing away and turning if it encounters a dropoff indicating a table edge.
|
3
|
+
|
4
|
+
# Check it out on YouTube: http://www.youtube.com/watch?v=uQ4BwxH7uoY
|
5
|
+
# (it's pretty straightforward).
|
6
|
+
|
7
|
+
require "lib/rubotz"
|
8
|
+
include Rubotz
|
9
|
+
|
10
|
+
describe Rubotz do
|
11
|
+
before(:each) do
|
12
|
+
@the_main_program_in_nxc =<<THE_MAIN_PROGRAM_IN_NXC
|
13
|
+
#include "NXCDefs.h"
|
14
|
+
// the robot drives around the table scanning its ultrasonic sensor
|
15
|
+
task main() {
|
16
|
+
short ultrasonic_sensor;
|
17
|
+
SetSensorLowspeed(S4);
|
18
|
+
while(true) {
|
19
|
+
ClearScreen();
|
20
|
+
ultrasonic_sensor = SensorUS(S4);
|
21
|
+
if (ultrasonic_sensor > 30) {
|
22
|
+
TextOut(0, LCD_LINE7, "Danger");
|
23
|
+
NumOut(0, LCD_LINE8, ultrasonic_sensor);
|
24
|
+
PlayFile("! Attention.rso");
|
25
|
+
OnRevSync(OUT_BC, 25, 0);
|
26
|
+
Wait(1500);
|
27
|
+
OnRevSync(OUT_BC, 25, 100);
|
28
|
+
Wait(325);
|
29
|
+
}
|
30
|
+
if (ultrasonic_sensor <= 30) {
|
31
|
+
TextOut(0, LCD_LINE7, "Safe");
|
32
|
+
NumOut(0, LCD_LINE8, ultrasonic_sensor);
|
33
|
+
OnFwdSync(OUT_BC, 35, 5);
|
34
|
+
}
|
35
|
+
Wait(25);
|
36
|
+
}
|
37
|
+
}
|
38
|
+
THE_MAIN_PROGRAM_IN_NXC
|
39
|
+
end
|
40
|
+
|
41
|
+
it "generates unindented single-threaded code for a robot which wanders a table without falling off" do
|
42
|
+
(Rubotz.program do
|
43
|
+
the_robot "drives around the table scanning its ultrasonic sensor"
|
44
|
+
initializing do
|
45
|
+
short ultrasonic_sensor
|
46
|
+
activate(UltrasonicSensor.port(4))
|
47
|
+
end
|
48
|
+
looping do
|
49
|
+
clear_screen
|
50
|
+
monitoring(UltrasonicSensor) do |ultrasonic_sensor|
|
51
|
+
ultrasonic_sensor.> 30 do
|
52
|
+
display(:line => 7, :text => "Danger")
|
53
|
+
display(:line => 8, :number => ultrasonic_sensor)
|
54
|
+
sound("! Attention.rso")
|
55
|
+
motor(:synchronized => true,
|
56
|
+
:direction => :reverse,
|
57
|
+
:ports => [:B, :C],
|
58
|
+
:power => 25,
|
59
|
+
:turn => 0)
|
60
|
+
wait(1500)
|
61
|
+
motor(:synchronized => true,
|
62
|
+
:direction => :reverse,
|
63
|
+
:ports => [:B, :C],
|
64
|
+
:power => 25,
|
65
|
+
:turn => 100)
|
66
|
+
wait(325)
|
67
|
+
end
|
68
|
+
ultrasonic_sensor.<= 30 do
|
69
|
+
display(:line => 7, :text => "Safe")
|
70
|
+
display(:line => 8, :number => ultrasonic_sensor)
|
71
|
+
motor(:synchronized => true,
|
72
|
+
:direction => :forwards,
|
73
|
+
:ports => [:B, :C],
|
74
|
+
:power => 35,
|
75
|
+
:turn => 5)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
wait(25)
|
79
|
+
end
|
80
|
+
end).gsub(/\s+/, " ").should == @the_main_program_in_nxc.gsub(/\s+/, " ")
|
81
|
+
end
|
82
|
+
|
83
|
+
it "indents generated code correctly" do
|
84
|
+
# (Rubotz.indent(@the_main_program_in_nxc.gsub(/\s+/, " "))).should == @the_main_program_in_nxc
|
85
|
+
pending "implement"
|
86
|
+
end
|
87
|
+
|
88
|
+
it "somehow merges its indentation with the indentation it gets from ERb" do
|
89
|
+
pending "I have no idea"
|
90
|
+
end
|
91
|
+
|
92
|
+
it "creates initialization sections with arbitrary code" do
|
93
|
+
(Generator.new do
|
94
|
+
initializing do
|
95
|
+
short ultrasonic_sensor
|
96
|
+
end
|
97
|
+
end).initialization_section.should == "short ultrasonic_sensor;\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
it "creates loop bodies with arbitrary code" do
|
101
|
+
(Generator.new do
|
102
|
+
looping do
|
103
|
+
short ultrasonic_sensor
|
104
|
+
end
|
105
|
+
end).loop_body.should == "short ultrasonic_sensor;\n"
|
106
|
+
end
|
107
|
+
|
108
|
+
it "declares short variables" do
|
109
|
+
(Rubotz.generate {short ultrasonic_sensor}).should == "short ultrasonic_sensor;\n"
|
110
|
+
end
|
111
|
+
|
112
|
+
it "has a #the_robot method which generates comments" do
|
113
|
+
(Generator.new do
|
114
|
+
the_robot "drives around the table scanning its ultrasonic sensor"
|
115
|
+
end).comment.should == "// the robot drives around the table scanning its ultrasonic sensor\n"
|
116
|
+
end
|
117
|
+
|
118
|
+
it "activates the ultrasonic sensor" do
|
119
|
+
(Rubotz.generate {activate(UltrasonicSensor.port(4))}).should == "SetSensorLowspeed(S4);\n"
|
120
|
+
end
|
121
|
+
|
122
|
+
it "generates variables and ifs for testing the ultrasonic sensor (without indentation)" do
|
123
|
+
ultrasonic_nxc =<<ULTRASONIC_NXC
|
124
|
+
ultrasonic_sensor = SensorUS(S4);
|
125
|
+
if (ultrasonic_sensor > 30) {
|
126
|
+
TextOut(0, LCD_LINE7, "greater than 30");
|
127
|
+
}
|
128
|
+
if (ultrasonic_sensor < 23) {
|
129
|
+
TextOut(0, LCD_LINE7, "less than 23");
|
130
|
+
}
|
131
|
+
if (ultrasonic_sensor >= 30) {
|
132
|
+
TextOut(0, LCD_LINE7, "greater than or equal to 30");
|
133
|
+
}
|
134
|
+
if (ultrasonic_sensor <= 23) {
|
135
|
+
TextOut(0, LCD_LINE7, "less than or equal to 23");
|
136
|
+
}
|
137
|
+
ULTRASONIC_NXC
|
138
|
+
(Rubotz.generate do
|
139
|
+
monitoring(UltrasonicSensor) do |ultrasonic_sensor|
|
140
|
+
ultrasonic_sensor.> 30 do
|
141
|
+
display(:line => 7, :text => "greater than 30")
|
142
|
+
end
|
143
|
+
ultrasonic_sensor.< 23 do
|
144
|
+
display(:line => 7, :text => "less than 23")
|
145
|
+
end
|
146
|
+
ultrasonic_sensor.>= 30 do
|
147
|
+
display(:line => 7, :text => "greater than or equal to 30")
|
148
|
+
end
|
149
|
+
ultrasonic_sensor.<= 23 do
|
150
|
+
display(:line => 7, :text => "less than or equal to 23")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end).gsub(/\s+/, " ").should == ultrasonic_nxc.gsub(/\s+/, " ")
|
154
|
+
end
|
155
|
+
|
156
|
+
it "generates clear screen code" do
|
157
|
+
(Rubotz.generate {clear_screen}).should == "ClearScreen();\n"
|
158
|
+
end
|
159
|
+
|
160
|
+
it "generates display code for text and numbers" do
|
161
|
+
(Rubotz.generate {display(:line => 7,
|
162
|
+
:text => "Safe")}).should == "TextOut(0, LCD_LINE7, \"Safe\");\n"
|
163
|
+
(Rubotz.generate {display(:line => 8,
|
164
|
+
:number => 25)}).should == "NumOut(0, LCD_LINE8, 25);\n"
|
165
|
+
end
|
166
|
+
|
167
|
+
it "generates display code with inline variables" do
|
168
|
+
(Rubotz.generate {display(:line => 8,
|
169
|
+
:number => ultrasonic_sensor)}).should == "NumOut(0, LCD_LINE8, ultrasonic_sensor);\n"
|
170
|
+
end
|
171
|
+
|
172
|
+
it "generates play sound code" do
|
173
|
+
(Rubotz.generate {sound("! Attention.rso")}).should == "PlayFile(\"! Attention.rso\");\n"
|
174
|
+
end
|
175
|
+
|
176
|
+
it "generates motor control code" do
|
177
|
+
(Rubotz.generate {motor(:synchronized => true,
|
178
|
+
:direction => :reverse,
|
179
|
+
:ports => [:B, :C],
|
180
|
+
:power => 25,
|
181
|
+
:turn => 100)}).should == "OnRevSync(OUT_BC, 25, 100);\n"
|
182
|
+
(Rubotz.generate {motor(:synchronized => true,
|
183
|
+
:direction => :forwards,
|
184
|
+
:ports => [:B, :C],
|
185
|
+
:power => 25,
|
186
|
+
:turn => 100)}).should == "OnFwdSync(OUT_BC, 25, 100);\n"
|
187
|
+
(Rubotz.generate {motor(:direction => :forwards,
|
188
|
+
:ports => [:B, :C],
|
189
|
+
:power => 50)}).should == "OnFwdSync(OUT_BC, 50, 0);\n"
|
190
|
+
(Rubotz.generate {motor(:direction => :banana,
|
191
|
+
:ports => :B,
|
192
|
+
:power => "houseplant")}).should_not == "OnFwdSync(OUT_BC, 50, 0);\n"
|
193
|
+
end
|
194
|
+
|
195
|
+
it "generates motor control code with inline variables" do
|
196
|
+
(Rubotz.generate {motor(:direction => :forwards,
|
197
|
+
:ports => [:B, :C],
|
198
|
+
:power => power,
|
199
|
+
:turn => turn)}).should == "OnFwdSync(OUT_BC, power, turn);\n"
|
200
|
+
end
|
201
|
+
|
202
|
+
it "generates wait commands" do
|
203
|
+
(Rubotz.generate {wait(325)}).should == "Wait(325);\n"
|
204
|
+
(Rubotz.generate {wait(1000)}).should == "Wait(1000);\n"
|
205
|
+
end
|
206
|
+
|
207
|
+
it "generates wait commands with inline variables" do
|
208
|
+
(Rubotz.generate {wait(delay)}).should == "Wait(delay);\n"
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rubotz
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Giles Bowkett
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-01-12 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: gilesb@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
files:
|
25
|
+
- config
|
26
|
+
- config/nxt_home.yaml
|
27
|
+
- examples
|
28
|
+
- examples/table_wanderer.rb
|
29
|
+
- lib
|
30
|
+
- lib/rubotz
|
31
|
+
- lib/rubotz/generator.rb
|
32
|
+
- lib/rubotz/motor_generator.rb
|
33
|
+
- lib/rubotz/ultrasonic_sensor.rb
|
34
|
+
- lib/rubotz.rb
|
35
|
+
- README
|
36
|
+
- rubotz-0.0.1.gem
|
37
|
+
- rubotz.gemspec
|
38
|
+
- slow_spec
|
39
|
+
- slow_spec/compiler_spec.rb
|
40
|
+
- spec
|
41
|
+
- spec/generating_spec.rb
|
42
|
+
- spec/table_wanderer_spec.rb
|
43
|
+
- templates
|
44
|
+
- templates/simple_while.nxc.erb
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://rubotz.rubyforge.org
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
version:
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
version:
|
64
|
+
requirements: []
|
65
|
+
|
66
|
+
rubyforge_project: rubotz
|
67
|
+
rubygems_version: 1.0.0
|
68
|
+
signing_key:
|
69
|
+
specification_version: 2
|
70
|
+
summary: Code generator for Lego Mindstorms NXT. Proof-of-concept release only.
|
71
|
+
test_files: []
|
72
|
+
|