Calico Myro

From IPRE Wiki

Jump to: navigation, search

Myro is an interface for programming robots. It is implemented in many languages and designed for use in Introductory Computing courses. It is being developed by the Institute for Personal Robots in Education.

This page describes the Calico Python interface.

If you were familiar with the previous version of Python Myro, please see Calico Differences.

Contents

Getting Started

All the commands from Myro are available in Python by importing them. Here is a quick start list:

  1. Double-click on the "calico.bat" file in the Calico folder on the desktop
  2. Enter: from Myro import *
  3. Turn on the robot, and connect via Bluetooth (using outgoing port number)
  4. Now, you can enter init("COM4") port of your robot, where "COM4" might be another string (see Calico Download#Optional: Using Calico with Robots) or "sim" (see below)

Simulation

Calico comes with a simulator, so you can do most of the robot activities without a real robot. See Calico Simulator for more details.

Testing

To test that everything is working, try:

python> from Myro import *

If you have a gamepad:

python> gamepad()

Otherwise:

python> joystick()

Manual Drive

joystick(): opens a joystick window; click and drag to move robot.

Output Functions

beep(self, duration, frequency, frequency2 = None): make a tone. If two tones are given, the robot will combine them.

speak(message, async = 0) - text-to-speech, turns message into spoken words

getVoice() - get the voice of the current speaker

getVoices() - get a list of all of the possible voices

getVoiceNames() - get a list of all of the possible voice names

setVoice(name) - set the voice to a known voice by name

setVoiceName(name) - set the voice to a known voice by name

Input Functions

ask(item) - will ask one time for item(s)

python> ask("Name")
(window pops up, user enters Sarah, presses Ok button)
'Sarah'

input(prompt) - will prompt user for input. Identical to ask. This always returns a string.

python> input("How old are you? ")
How old are you? 19
'19'

Note that input() and ask() both return strings. You can use the eval() function to turn strings into values. For example:

python> eval(input("How old are you? "))
How old are you? 19
19

This gives you the number 19, rather than the string '19'. Also:

python> eval(input("Enter x,y: "))
Enter x,y: 34, 56
(34, 56)

In this example, eval will turn the string '34, 56' into the tuple of numbers (34, 56).

askQuestion(question, [answerList]) - prompt a question and return answer.

python> askQuestion("Are you ready?")
(window pops up, users selects Yes)
'Yes'
python> askQuestion("How many lumps would you like?", ["One", "Two", "Three"])
(window pops up, user selects Three)
'Three'

yesno("Are you ready?") - asks a yes/no question and provides two buttons, "Yes" and "No". Returns "Yes" or "No". Same as askQuestion() with no answerList.

python> yesno("Are you ready?")
(window pops up, users selects Yes)
'Yes'

Movement Functions

forward(amount, seconds): move forward, stop any rotation, for number of seconds

python> forward(1, .5)

backward(amount, seconds): move backward, stop any rotation, for number of seconds

python> backward(.9, 2)

turnLeft(amount, seconds): turn left, stop any forward movement, for number of seconds

python> turnLeft(.4, 1)

turnRight(amount, seconds): turn right, stop any forward movement, for number of seconds

python> turnRight(.5, 1)

stop(): stop all movement

python> stop()

translate(amount): move forward and backwards. 0 to 1 moves forward; 0 to -1 moves backwards rotate(amount): turn left or right. 0 to 1 turns left, 0 to -1 turns right

NOTE: translate and rotate are independent, although they may both effect each the wheel. That means that a translate() and a rotate() will blend into a meaningful combination.

python> translate(1)  # full speed ahead
python> translate(-1) # full speed backwards
python> translate(0)  # stop in the translate direction
python> rotate(.5)  # half-speed to the left
python> rotate(-1)  # full speed to the right

move(translate, rotate): rotate and translate

python> move(0, 1)  # turn full speed to left
python> move(0, -1) # turn full speed to right
python> move(1, 1)  # turn full speed to left while moving full speed ahead
python> move(.5, 0) # go forward half speed

motors(left, right): control the left and right motors

Reading Sensors

getLight(pos): read a light sensor on the scribbler; defaults to "all"

getIR(pos): read an IR sensor on the scribbler; defaults to "all"

getLine(pos): read line sensor on the scribbler; defaults to "all"

getStall(): read stall sensor on the scribbler

NOTE: Every time you issue a move command, the stall sensor resets, and it needs to wait a short time to see whether the motors are stalled. This means that the sensor won’t give accurate results if you test it too soon after the robot starts to move.

getName(): read the robot's name

getPassword(): read the robot's password

getAll(): read all positions of all of the major sensors; returns a dictionary

getVolume(): returns 0 or 1

getData(): get some bytes stored in the robots memory

getInfo(): retrieve information about the robot

getBright("left" | "middle" | "center" | "right" | 0 | 1 | 2): read one of the Fluke's virtual light sensors. The Fluke's virtual light sensors report the total intensity in the left, center, and right sides of the Fluke's camera.

getObstacle("left" | "middle"| "center" | "right" | 0 | 1 | 2): read one of the Fluke's IR obstacle sensors (see setIRPower below). Higher values mean that IR light is being reflected (e.g an obstacle is detected), a low value means IR is not being reflected and there seems to be open space in that direction. The value ranges from 0 to 6400.

getBattery() gets the voltage of the battery (note: If the battery drops below ~6.1V the Fluke's back LED will flash to alert you to change or preferably recharge your batteries)

get(sensor): read the sensor "stall"; or get all readings of "ir", "light", or "line"; or get "all" which is all of those. Also can get "config".

get("config"): returns meta data about the robot's hardware

get(sensor, pos): read any of the following sensors or items by name and position

python> get("stall")
0
python> get("light", 0)
128
python> get("line")
[0, 0]
python> get("all")
{'light': [235, 13], 'line': [1, 0], 'ir': [0, 0], 'stall': 0}
python> get("all") # using a fluke
{'battery': 6.2436550642715174, 'light': [13176, 3058, 1848], 'ir': [1, 1], 'obstacle': [0, 0, 0], 
 'bright': [193536, 193536,  193536], 'stall': 0, 'blob': (0, 0, 0), 'line': [1, 1]}

Setting Values

setLED(position, value): set a LED

python> setLED("left", "on")
python> setLED("right", "off")

setName(name): set the robot's name (limit of 16 characters)

python> setName("Howie")

setVolume(level): set the speaker's volume (0/"off" or 1/"on")

python> setVolume("off")
python> setVolume(0)
python> setVolume("on")
python> setVolume(1)

setData(position, value): set a byte of data in the robot's memory to a value between 0 and 255.

setLEDFront(value): turn on the led on the front of the Fluke (0 for off and or 1 for on)

python> setLEDFront(1) # turn on the Fluke's front LED
python> setLEDFront(0) # turn off the Fluke's front LED
python> set("led", "front", 0) # turn off the Fluke's front LED

setLEDBack(value): turn on the LED on the back of the Fluke. The brightness of this LED is configurable between 0-1.

python> setLEDBack(0.5) # turn on the Fluke's back LED at 1/2 brightness
python> setLEDBack(1.0) # turn on the Fluke's back LED at full brightness
python> set("led", "back", 0) #turn off the Fluke's back LED

setIRPower(power): set the output power of the Fluke's IR obstacle sensors (defaults to 135). If getObstacle() always reports high values try lowering the IR output power. If you always receive a zero value, try increasing the power. The power value should be between 0 and 255.

python> setIRPower(135)
python> setIRPower(140)

darkenCamera(level): turn off the camera's auto-exposure, auto-gain, and lower the gain to "level:. This is useful when using the getBright() virtual light sensors.

manualCamera(gain=0, brightness=128, exposure=65): turn off the camera's auto-exposure, auto-gain, and auto-white balance, and set the gain (integer in the range 0-63), brightness (integer in the range 0-255), and exposure manually (integer in the range 0-255). (In version 2.8.3 of Myro)

autoCamera(): turn on the auto-exposure, auto-gain, and auto-color-balance.

set(item, value): set a value (for "name", and "volume")

set(item, position, value): set a value (for "led")

python> set("name", "Duckman")
python> set("led", "center", "off")
python> set("volume", "off")

Flow of Control

If you wished to perform a loop for 5 seconds, you could use the following idiom:

for seconds in timer(5):
    print "running for", seconds, "..."

Image processing

The following objects and functions are related to the camera functions on the Scribbler. There are two different interfaces: the Multimedia interface (largely based on Mark Guzdial's introductory book), and the Graphics Object interface (largely based on John Zelle's introductory book). These are independent; however, there are methods to move between the two. The first library is built on the second.

Creating a picture, manually, from a file, or from the robot:

picture = takePicture("color" | "gray" | "blob") # gets image from robot
picture = makePicture(filename)                  # reads in a image file, examples: PNG, JPG, GIF
picture = makePicture(columns, rows)             # creates a blank picture
picture = makePicture(columns, rows, array)      # creates a new picture from an array (a column-major sequence of 0-255 values that represent Red, Green, Blue (in that order))
picture = makePicture(columns, rows, array, mode)# creates a new picture from an array, where mode = "color", or "gray"
pictureCopy = copyPicture(picture)               # creates a copy of picture

Output:

show(picture)
savePicture(picture, filename)
savePicture([picture, picture, ...], filename)   # creates an animated GIF

The show(picture) function has a couple of mouse functions. You can click with the left mouse on a pixel and you can see "(x,y): (r,g,b)" in the window's status bar. If you click and drag a rectangle in the window, you will set the blob-tracking colors to the ranges of colors inside the bounding box.

show() can also take an optional name:

show(pic2, "name2")

so that you can show more than one image at a time.

Image dimensions:

int_value = getWidth(picture)
int_value = getHeight(picture)

Creating and manipulating color objects:

color = makeColor(r, g, b)
color = makeColor("name")
color = makeColor("#hexcode")
color = pickAColor()
color = getColor(pixel)
setColor(pixel, color)

Pixel manipulation:

pixel = getPixel(picture, x, y)
pixels = getPixels(picture)
int_value = getRed(pixel)
int_value = getGreen(pixel)
int_value = getBlue(pixel)
int_value = setRed(pixel, color)
int_value = setGreen(pixel, color)
int_value = setBlue(pixel, color)
int_value = getX(pixel)
int_value = getY(pixel)
r, g, b = getRGB([color | pixel])

Red, green, and blue int_values are between 0 and 255, inclusive.

The getPixels() function is designed to be used with a for-statement, like so:

for pixel in getPixels(picture):
    # do something with each pixel

getPixels() returns a generator object. To get a pixel by index, you could first turn it into a list:

mylist = list(getPixels(picture))

You can also save any picture to a file:

savePicture(picture, filename)    # does the same as above, but has intuitive name

copyPicture() is effectively defined as:

def copyPicture(picture):
    newPicture = makePicture(getWidth(picture), getHeight(picture))
    for x in range(getWidth(picture)):
        for y in range(getHeight(picture)):
            setColor(getPixel(newPicture, x, y), getColor(getPixel(picture, x, y)))
    return newPicture

Examples

Processing by rows and cols:

from Myro import *
picture = makePicture(pickAFile())
show(picture)
for i in range(getWidth(picture)):
    for j in range(getHeight(picture)):
        pixel = getPixel(picture, i, j)
        setGreen(pixel, 255)

Processing by each pixel:

from Myro import *
picture = makePicture(pickAFile())
show(picture)
for pixel in getPixels(picture):
    setGreen(pixel, 255)

Advanced Vision Functions

The Fluke can not only take images, but it can do some very simple image processing. In particular, a simple form of on-board image segmentation. If you want to track something in an image based on its color or brightness, the on-board color segmentation can be very useful since its faster than doing it in python. By default, the Fluke is set to track pink objects. You can also graphically select an object in the image to track. First use the show(picture) function and then drag a box around the object you want to track, the software will automatically determine the color bounding box.

We can use the Fluke's computer vision in two ways. First, we can grab a "blob" image from the Fluke. This image is a black and white image with the white pixels being part of the object of interest. Blob images can be transmitted faster than a full color picture.

 p = takePicture()
 show(p)
 # select object in the image
 b = takePicture("blob")
 show(b)    

For instance, here we see two images of a dog and her toy. The first is a regular color picture and the second is a blob image with the pink dog toy selected.

Image:lucy.jpg Image:pig.jpg

Another useful function is getBlob() that will return three items, the total number of pixels that fell inside the bounding box, and the average x and y locations of those pixels.

 pixel_count, average_x, average_y = getBlob()

Rather than using the mouse to select the bounding box for segmentation, you can manually configure the Fluke using the configureBlob() function call:

 configureBlob(y_low = 0, y_high = 255, u_low = 0, u_high = 255, v_low = 0, v_high = 255)

The parameters to configureBlob() create a bounding box in YUV space. Instead of using RGB which stands for Red/Green/Blue, YUV is an alternate way to describe color. The Y component contains the brightness or intensity information of the pixel, the U/V components contain the color.

Finally, you can also use getBlob() to locate bright pixels. We do this by segmenting bright pixels, meaning the Y components of the pixels are large :

 configureBlob(y_low=100, y_high=255)
 b = takePicture('blob')
 show(b)
 pxs, avg_x, avg_y = getBlob()

Graphics Objects Interface

The object oriented graph window can be used to create your own drawings:

win = Window(title, width, height)        # Alternately Window(title) or Window()
win.setBackground(color)
win.close()
win.getMouse()                            # Returns a Point object of where the mouse is clicked

To get additional functionality, you will need to:

from Graphics import *

Then you can do the following graphics examples.

Once you have a graph window created you can draw the following Graph Objects on it with their .draw() method:

Line(point1, point2)
Circle(centerPoint, radius)
Rectangle(topLeftPoint, bottomRightPoint) 
Oval(centerPoint, xradius, yradius)                       
Polygon(point1, point2, point3, ...) 
Text(centerPoint, string)
Picture(width, height[, color])
Arrow(centerPoint)
Curve(p1, p2, p3, p4)

See the Calico Graphics for more details.

Example

Using the graphics window to draw:

from Myro import *
from Graphics import *
win = Window("My Window", 500, 500)         # Window(string, width, height)
color = Color(100, 200, 50)                 # red, green, and blue are values from 0 to 255
win.setBackground(color)                    # sets background color of window to the color we created
point = Point(100,25)                       # creates a point referencing location (100,25)
message = Text(point, "Click on window")    # creates message "Click on window" anchored at location (100,25)
message.draw(win)                           # puts the message on the graph window
click = win.getMouse()                      # Waits for user's mouse click and returns as a Point object
message.undraw()                            # erases message from the graph window
text = Text(click, "Some Text")             # creates text "Some Text" anchored at location you clicked
text.draw(win)                              # adds the text to the graph window
p = Point(150,150)                          # creates the point for top left corner of the oval's bounding box
xradius = 50
yradius = 20
oval = Oval(p, xradius, yradius)            # creates the oval
redColor = Color(255,0,0)                   # defines the color red
oval.fill = redColor                        # colors the oval red
oval.draw(win)                              # draws the oval to the graph window
wait(3)                                     # Window will close in 3 seconds

Miscellaneous commands

wait(seconds) - Pause for the given amount of seconds. Seconds can be a decimal number.

python> wait(5)
[5 seconds go by]
Ok
python> 

currentTime() - the current time, in seconds from an arbitrary starting point in time, many years ago. Can you figure out the date of the start time?

python> currentTime()
1164956956.2690001

odd(n) - returns True if n is odd

even(n) - returns True if n is even

Random decisions

flipCoin() - Returns "heads" or "tails" randomly.

python> flipCoin()
'tails'
python> flipCoin()
'tails'
python> flipCoin()
'heads'

heads() - returns True 50% of the time

tails() - returns True 50% of the time

pickOne(value) or pickOne(value1, value2, ...) - Returns a number or element randomly. New in Myro 1.0.2

python> pickOne(5)
0 # randomly returns 0 through 4 evenly over time
python> pickOne(2, 4, 6, 8)
6 # randomly returns 2, 4, 6, or 8 evenly over time
python> pickOne("red", "white", "blue", "green")
6 # randomly returns "red", "white", "blue", or "green" evenly over time

randomNumber() - Returns a random number between 0 (inclusive) and 1 (exclusive).

python> randomNumber()
0.65218366496862357

File and Folder Functions

pickAFolder() pop up window.
Enlarge
pickAFolder() pop up window.

pickAFolder() - allows you to select a folder

python> pickAFolder()
'C:/Python24'
pickAFile() pop up window
Enlarge
pickAFile() pop up window

pickAFile() - allows you to select a file

python> pickAFile()
'C:/Python24/README.txt'


Media Functions

readSong(filename) - reads a file in the Song File Format and returns a list of tuples.

makeSong(text) - make a song in the Song File Format where lines are separated by semicolons.

python> makeSong("c 1; g 1/4; a 1/2; e 3/4;")
[(523.29999999999995, 1.0), (784.0, 0.25), (880.0, 0.5), (659.29999999999995, 0.75)]
python> singsong = makeSong("c 1; g 1/4; a 1/2; e 3/4;")

saveSong(text or song, filename) - saves a song in tuple format, or in text format

Audio

Using Myro, you can add sounds to your programs. The easiest way is to just play a file:

Myro.play(filename)

Plays a .wav, .ogg, .mp3, .mod or .mid file.

The rest of this section uses a sound object to have more control.

sound = Myro.makeSound(filename) 

Loads a .wav, .ogg, .mp3, .mod or .mid file into memory.

bytes = sound.Array()

Returns sound as an array of bytes.

channel = sound.Play()

Plays the sound.

channel = sound.Play(loops)

Plays the sound for a desired number of loops.

channel = sound.Play(loopIndefinitely)

Plays the sound.

channel = sound.Play(loops, milliseconds)

Plays a sound for a desired number of milliseconds or loops.

channel = sound.FadeIn(milliseconds)

Fades in a sample once using the first available channel.

channel = sound.FadeIn(milliseconds, loops)

Fades in a sample the specified number of times using the first available channel.

channel = sound.FadeInTimed(milliseconds, ticks)

Fades in a sample once using the first available channel, stopping after the specified number of ms.

channel = sound.FadeInTimed(milliseconds, loops, ticks)

Fades in a sample the specified number of times using the first available channel, stopping after the specified number of ms.

sound.Stop()

Stops the sound sample.

sound.Fadeout(fadeoutTime)

Fades out the sound sample.

Tones

Myro.beep(duration, frequency)
Myro.beep(duration, frequency1, frequency2)

Plays a tone for a given amount of time. If duration is zero, then it plays until you stop it.

Myro.play(duration, function)

Plays the wave form generated by function. function is a function that takes an array and an index. It then fills the array with bytes (0 to 255) which represent the wave form.

Example:

import Myro
import Graphics
import math

def makeTone(freq1, freq2, freq3):
    """
    Make a function that is based on the given frequencies.
    """
    # Each time slice is 1/playbackfreq of 2pi
    slice = (1/44100 * 360 * math.pi/180) # time in radians
    def wave(array, position):
        """
        The actual function that will compute the wave.
        """
        # Fill the array with bytes (unsigned 8-bit values)
        for i in range(len(array)):
            angle1 = position * slice * freq1
            angle2 = position * slice * freq2
            angle3 = position * slice * freq3
            array[i] = ((127 + math.cos(angle1) * 127) +
                        (127 + math.sin(angle2) * math.sin(angle1) * 127) +
                        (127 + math.cos(angle3) * 127))/3
            position += 1
    return wave

def plotSound(function, width=500):
    win = Myro.Window("Sound Plot", width, 255)
    array = [0] * width
    function(array, 0)
    prev = (0, 255)
    for i in range(width):
        Graphics.Line(prev, (i, array[i])).draw(win)
        prev = (i, array[i])

tone = makeTone(440, 440, 220)
plotSound(tone)

Myro.play(2, tone)

Gamepad

There is a pre-programmed gamepad() function for immediately driving the robot:

python> gamepad()

This will show a menua of options (depending on the type of gamepad your have). Allows speaking, moving the robot, and playing tones.

Also, one can write quick gamepad utilities with:

python> gamepad({"button": function, ...})

where "button" is one of the names of items that a gamepad can return, and function is a function that takes the button values as a list. For example:

def buttonHandler(args):
    # args is a list of values of the item being handled
    ....

def axisHandler(args):
    # args is a list of values of the item being handled
    ....

gamepad({"button": buttonHandler, "axis": axisHandler})

Whenever a axis or button is pressed or released, then the handlers will be called.

There are two additional commands: getGamepad() and getGamepadNow()

  1. getGamepad() waits for an event before returning (ie, is blocking),
  2. getGamepadNow() will immediately return the current state of the gamepad.

Gamepad Examples

python> getGamepad("count")
2
python> getGamepad("button") # waits till you press at least one button
[0, 0, 1, 0, 1, 0, 0, 0]
python> getGamepad(1, "button") # waits till ID 1 presses at least one button
[1, 0, 0, 0, 0, 0, 0, 0]
python> getGamepad(range(getGamepad("count")), "button")
# waits till someone presses a button
[[0, [1, 0, 0, 0, 0, 0, 0, 0]],
 [1, [0, 0, 0, 0, 0, 0, 0, 0]]]
python> getGamepad(["button", "axis"])
{"button": [1, 0, 0, 0, 0, 0, 0, 0],
 "axis": [-0.999969482421875, 0.0]}
(sometimes axis doesn't return exactly 1 or -1).

Here is a short, useful program:

python> while 1: move(*getGamepad("robot"))

Here is a more functional one:

done = False
while not done:
    results = getGamepad(["button", "robot"])
    move(*results["robot"])
    if results["button"][1]: beep(.5, 440)
    if results["button"][2]: beep(.5, 880)
    done = (results["button"][0] == 1)

Gamepad Details

  1. getGamepad("count") - returns (immediately) the number of gamepads connected
  2. getGamepad(ID, [ITEM, ...]) - return the ITEMs for gamepad ID. ID can be left out and will default to 0, the first one. If you request more than one ITEM, then they come back in a dictionary. Just request one ITEM and you'll get the value (as a list, string, or number).
  3. getGamepad([ID1, ID2...], [ITEM, ...]) - return the ITEMs for gamepad IDs as a list of lists of ID, RESULTS. For example:
    python> getGamepad([0, 1], ["button", "axis"])
    [{'button': [0, 0, 0, 0, 0, 0, 0, 0], 'axis': [0.0, 1.0]}, {'button': [1, 1, 0, 0, 0, 0, 0, 0], 'axis': [-1.0, -1.0]}]
    python> getGamepad([0, 1], "axis")
    [[0.0, 1.0], [-1.0, -1.0]]

ITEM can be:

  1. "count" - returns (immediately) number of gamepads plugged in
  2. "robot" - returns axis values (floats) ready to be used by move() Myro command as [translate, rotate]
  3. empty - if nothing is passed in, it returns all of the following as a dictionary
  4. "name" - name of gamepad, as string
  5. "axis" - returns values of axis controller in a list (as floats)
  6. "ball" - returns values of ball controller in a list
  7. "button" - returns values of buttons in a list (as integers)
  8. "hat" - returns values of hat in a list

Here is a short little demo of how you could write a multi-player "game" where each player has a gamepad controller and appears as a circle on the screen.

from Myro import *
from Graphics import * # imports Circle, and Point

def game():
    win = Window("My Game!", 500, 500)
    numplayers = getGamepad("count")
    colors = ['red', 'blue', 'green', 'yellow', 'orange']
    players = []
    # Create the players:
    for p in range(numplayers):
        circle = Circle(Point(randomNumber() * 500,
                              randomNumber() * 500), 10)
        circle.fill = Color(colors[p])
        players.append(circle)
        players[-1].draw(win)
    # Let's play!
    speak("Red is it! Don't let red touch you!")
    while True:
        for data in getGamepadNow(range(numplayers), ["axis","button"]):
            for id in range(numplayers):
                players[id].move(data[id]["axis"][0] * 20,
                                 data[id]["axis"][1] * 20)
                if data[id]["button"][0]: # fire a missile
                    speak("Fire!")
        wait(.1)

The window produced with the show(picture) command allows mouse clicks.

Robot's Orientation

If you are using a bluetooth-serial adapter or a serial cable to control your robot, then the scribbler's normal forward direction is used. When using the Fluke the forward direction is flipped. "Forward" is in the direction of the Fluke's camera. However, the orientation of the scribbler can be manually changed using the setForwardness() function. This is particularly useful if you want to use the Scribbler's IR and light sensors.

setForwardness(orientation): orientation can be 0/"scribbler-forward" or 1/"fluke-forward"

getForwardness(): Returns the orientation of the robot "scribbler-forward" or "fluke-forward"

Personal tools