Advanced Cycle Tutorial
This tutorial follows on from the Basic Cycle
Tutorial. If you haven't already read this, then you may wish to do
so. In this Advanced Tutorial we will see how the definitions in
cybot.cyc relate to the code written in the Basic Tutorial.
Hello World Mk II
The simplest program possible in any programming language is usually Hello
World! Whilst this may be true, we can complicate it by including the
definition for World in our code! As before our Hello World program will make
Cybot's antennae LEDs flash, but we will add the definition for the
antennae from cybot.cyc :
|
1:
2:
3: class LEDs
4: {
5: output toggle() : block( 9, "Toggle" );
6: }
7:
8: LEDs antennae;
9:
10: proc main()
11: {
12: antennae.toggle();
13: }
|
The first piece of code now is a declaration of a new class called
LEDs . Classes are used to define aspects of a robot. In
this case, we are describing an output device which is a set of LEDs. A new
class is declared with the keyword class followed by a
unique name for that class and a body enclosed in braces (curly
brackets). By convention, all class names start with a capital letter.
This helps distinguish them from procedure names, which begin with an initial
lowercase letter.
The body of a class is made up of a list of member
definitions, which describe what the class can do. Members can
be inputs (sensors), outputs (things like motors) or
processes (neither inputs nor outputs, but things which take time to
execute, like wait). At line 5, we declare a new output called
toggle . A member declaration is one of input ,
output or process , followed by a unique name (by
which that member can later be referred), followed by a set of
parenthesis (round brackets), which can contain parameter names,
followed by a colon (':') and a member definition. The definition
describes which type of block it output into the .03p file, and
is written as block followed, in brackets, by a number indicating
the type of block (in this case 9 for LEDs), and then a list of comma separated
parameters specific to the block being created (in this case the string
"Toggle" ). The definition is terminated by a semicolon (';').
What does this mean? Well, where-ever we make a call to
toggle() , the compiler will output an LEDs block with the action
set to Toggle. So how do we call toggle() ? We need an
instance of LEDs to call it from.
Where a class declares a type of sensor or output, an
instance declares a specific sensor on a particular robot. We can have
more than one instance of LEDs , each relating to a
particular set of LEDs, say one for the antennae and one for another set of
LEDs in the center of each wheel hub. This approach lets us define each aspect
of a robot once using a class, and the create instances for each
use of that aspect e.g. one for each Sonar sensor.
In out example, line 8 declares a new instance of LEDs
called antennae . An instance declaration is written as the
name of the class from which to create an instance, followed by a
unique name for the instance being created. Again, by convention, an
instance name starts with a lowercase letter. We will see later how it is
possible to parameterize this, so that each instance created is slightly
different.
Finally, if we look at the main program, we see that at line 12, we use the
instance of class LEDs called antennae
to call the member toggle .
Expanding The Class Declaration
If we now look at the complete declaration of LEDs , we can see
that it has two more output members, on , which turns
the LEDs on, and off , which unsurprisingly turns them off again.
|
1: class LEDs
2: {
3: output on() : block( 9, "On" );
4: output off() : block( 9, "Off" );
5: output toggle() : block( 9, "Toggle" );
6: }
|
The way this works is that each member passes a different
parameter to create a block set up to perform a particular action. By
grouping these together into one logical unit which describes what actions you
can perform on a set of LEDs, we can produce something which is useful and
above all re-usable.
Passing Parameters To Members
We will now examine how we can vary what happens when we call a particular
member by passing parameters. Take the call to
delay.wait() that we used in the Basic Tutorial. Here we could
pass in a number which was the number of seconds to pause for, before
continuing. So how was this implemented? Well, we can alter what is used to
create the block by using parameters:
|
1: class Timer
2: {
3: process wait( secs ) : block( 3, secs );
4: }
5:
6: Timer delay;
|
Here we see that at line 3, a process called wait is
being declared. However, instead of the empty parenthesis we had in our
example above we see that wait can take a single parameter.
This parameter can be referred to through the name secs and
we use it in the member definition so that it is placed in the Delay
block in the .03p file. This means that is we call
delay.wait( 2 ) , then the value of secs will be 2,
and the member definition effectively becomes
block( 3, 2 ) . What is more, if we used some other value than 2,
that too would find its way into the block .
Parameters can be used with inputs, outputs or
processes and there is no limit to the number of parameters you
can use, as long as the number of parameters passed in the call is the
same as the number of parameters in the member declaration.
Parameters are not limited to being number either, they can be strings
as well.
Inputs
Inputs are very similar to outputs and processes, but they
have an optional part which helps the compiler to produce valid output for
switch statements, particularly those that use a
default clause. Take the following input declaration:
|
1: class Light
2: {
3: input getStatus() : block( 10, 1, 1 );
4: }
5:
6: Light light;
|
Here we have declared an input called getStatus , which
would normally access with code like this:
|
1: proc main()
2: {
3: while( true )
4: {
5: switch( light.getStatus() )
6: {
7: case 1:
8: motors.setSpeed( -2, 2 );
9: break;
10: case 2:
11: motors.setSpeed( 0, 0 );
12: break;
// ...
14: }
15: }
16: }
|
How does the compiler know how may input values there are, so that it can
both determine valid values for the case statements. Also if we
don't supply enough case values for all the possible inputs,
or we use a default clause, how does the compiler know which
inputs to link to the next statement? The answer is to tell the compiler the
maximum and minimum acceptable values:
|
1: class Light
2: {
3: input{1,3} getStatus() : block( 10, 1, 1 );
4: }
5:
6: Light light;
|
At line 3, you can now see the addition of a pair of numbers in braces after
the input keyword. The first is the minimum value, the second is
the maximum, so in this example the values 1, 2, and 3 are all permissible.
Constants
There is one other kind of member which it is possible to declare, and
that is the constant. A constant is simply a number or a string
which is accessible through a unique name. Constants are useful as they
prevent code from depending upon the absolute values used, so if at some later
date the numbers or strings change, only the constant declaration needs to be
changed and not all of your code. Constants are declared using the
const keyword:
|
1: class Light
2: {
3: const LEFT = 1;
4: const SAME = 2;
5: const RIGHT = 3;
6:
7: input{1,3} getStatus() : block( 10, 1, 1 );
8: }
|
Here LEFT is defined to be 1, and where you would normally use
a number, you could equally use Light.LEFT , in this case it would
be in a switch statement, but constants can also be used in
place of parameters. Note that the name of the class is used to
denote where the constant is defined, and not the name of an
instance.
Passing Parameters To Classes
You may have asked yourself earlier how, if we create two instances of a
class, they can come to represent two different physical sensors. Well,
in short, they can't, at least not without using some kind of
parameters, as we saw using members. This is exactly how they
work, when we create an instance we can specify a list of
parameters which may be used in some or all of the members of the
class.
In the same way that a member declaration has a list of
parameters, a class declaration may optionally have a
parameter list too. A simple example of this is an alternative way of
implementing Cybot's Sonar sensors. The existing code is as follows:
|
1: class Sonar
2: {
3: input{1,4} getLeftRange() : block( 6, "Left" );
4: input{1,4} getRightRange() : block( 6, "Right" );
5: }
6:
7: Sonar sonar;
|
This can be modified to use class parameters, so the same code
can now be written as:
|
1: class Sonar( side )
2: {
3: input{1,4} getRange() : block( 6, side );
4: }
5:
6: Sonar leftSonar( "Left" );
7: Sonar rightSonar( "Right" );
|
As you can see, line 3 has been changed so that the class now takes a
parameter, side , which will be either "Left" or
"Right" . Now, to create an instance of the class
Sonar we pass a list of parameters in parenthesis after the
instance name. So in lines 6 and 7, we are creating an instance,
leftSonar , in which side will be "Left" ,
and another instance, rightSonar , where it will be
"Right" . The code which makes use of these input members
is now slightly different. Instead of calling
sonar.getLeftRange() we now call
leftSonar.getRange() . Both of these are perfectly valid ways of
implementing the same thing, but the former was chosen as it more closely
mimics the use of the motors.
|