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