Academy of Michigan Staff
Robofest Academy Staff
Sessions
Monday, January 9th:
In sessions at the end of last month, Dr. Chung and Mr. Tedder
have introduced all the relevant hardware and software. Today
we will present our objectives to the students and work out
a plan to accomplish the objectives. The key points emphasized
will be the importance of planning and the importance of teamwork
in completing an engineering task. The formal name for our
course would be, "Learning to program autonomous robots."
On a resume this would be, "Embedded Systems Programming" and
not, "Playing with Legos." Just last semester, one of my
students got a job because of his experience with this same
LEGO Mindstorms kit. The programming language we will use
is Not Quite C. NQC was invented by Dave Baum for his students
at the University of Utrecht. Our practice sessions will be
on the playing field for Robofest 2006.
In a world where a long term job commitment might be 1 year, and teamwork is much more common than solo work, becoming literate programmers is very important. We need to be able to communicate clearly what our programs do and how they do it. Otherwise they will be not much help to our team and useless, as soon as we move to our next job. Using #define's and // comments in NQC make programs easier to read. Use them!
Wednesday, January 11th:
Today we will have 2 missions
First, what does this program do and why doesn't it do what you might expect? How can we fix it?
// touches.nqc
// a program to count the number of times the touch sensor is pressed
#define TS SENSOR_1
#define FOREVER 0
int count;
task main()
{
SetSensor(TS,SENSOR_TOUCH);
count = 0;
SetUserDisplay(count,0);
until (count == 10) {
until (TS == 1) ;
count = count + 1;
// adding until (TS == 0) ; would keep the RCX from counting too fast
}
until (FOREVER) ; // keep the display on
}
Try stopping on a square of bright foil.
// foil.nqc
// a program to stop when you find a bright patch
#define LEFT OUT_A
#define RIGHT OUT_C
#define LS SENSOR_2
#define FOIL 60
task main()
{
SetSensor(LS,SENSOR_LIGHT);
OnFwd(LEFT+RIGHT);
start find_foil;
}
task find_foil()
{
until (LS >= FOIL) ;
Off(LEFT+RIGHT);
StopAllTasks();
}
Simple line following from Dave Baum. Does this work from either side of the line?
// line1.nqc
// a program to follow a line after the example by Dave Baum
// turn by stopping 1 wheel
#define LEFT OUT_A
#define RIGHT OUT_C
#define LS SENSOR_2
#define TOO_DARK 44
#define TOO_BRIGHT 48
task main()
{
SetSensor(LS,SENSOR_LIGHT);
OnFwd(LEFT+RIGHT);
while(true) {
if (LS <= TOO_DARK) {
Off(LEFT);
On(RIGHT);
} else if (LS >= TOO_BRIGHT) {
Off(RIGHT);
On(LEFT);
} else {
On(LEFT+RIGHT);
}
}
}
Wednesday, January 18th:
Today we will have one more mission.
Last week we combined foil.nqc and line1.nqc into something like
// line2.nqc
// a program to follow a line and stop when you find a bright patch
// turn by stopping 1 wheel
#define LEFT OUT_A
#define RIGHT OUT_C
#define LS SENSOR_2
#define TOO_DARK 44
#define TOO_BRIGHT 48
#define FOIL 60
task main()
{
SetSensor(LS,SENSOR_LIGHT);
start find_foil;
OnFwd(LEFT+RIGHT);
while(true) {
if (LS <= TOO_DARK) {
Off(LEFT);
On(RIGHT);
} else if (LS >= TOO_BRIGHT) {
Off(RIGHT);
On(LEFT);
} else {
On(LEFT+RIGHT);
}
}
}
task find_foil()
{
until (LS >= FOIL) ;
Off(LEFT+RIGHT);
StopAllTasks();
}
Today we will change find_foil a little. Before it could just stop. Now we will make it so it can say to the other tasks, "Hey, I found the foil!" (In programming this is called raising a flag or signalling with a semaphore.) For times when things are going wrong, adding a little sound can help the programmer figure out what is going on.
int foil;
task main()
{
...
foil = false;
start find_foil;
...
}
task find_foil()
{
until (LS >= FOIL) ;
foil = true; // let task main know what happened
PlaySound(SOUND_UP); // let person know what happened
}
A new task to find the bottle is similar.
int bottle;
task main()
{
...
bottle = false;
start find_bottle_by_touch;
...
}
task find_bottle_by_touch()
{
until (TS == 1) ;
bottle = true; // let task main know what happened
PlaySound(SOUND_DOUBLE_BEEP); // let person know what happened
}
A task is one kind of building block. A function is another kind of NQC building block. Both tasks and functions are just blocks or paragraphs of code statements. English sentences end with a period and NQC statements end with a semicolon. English paragraphs begin with an indentation and end with a blank line. NQC blocks begin with a { and end with a }. Functions run one after the other. Tasks run at the same time. The main task in line1.nqc followed a line. At the same time, the find_foil task kept a watch for the patch of foil. Tasks of course start with the word "task". In NQC, all functions start with the word "void".
Let us make line following into a task of its own. Let us make waiting to start into a function. For the start, instead of counting up, let us count down like at a rocket launch.
#define LEFT OUT_A
#define RIGHT OUT_C
#define LS SENSOR_2
#define TOO_DARK 44
#define TOO_BRIGHT 48
task follow_line()
{
SetSensor(LS,SENSOR_LIGHT);
OnFwd(LEFT+RIGHT);
while(true) {
if (LS <= TOO_DARK) {
Off(LEFT);
On(RIGHT);
} else if (LS >= TOO_BRIGHT) {
Off(RIGHT);
On(LEFT);
} else {
On(LEFT+RIGHT);
}
}
}
int count;
void wait_start()
{
count = 2;
SetUserDisplay(count,0);
until (count == 0) {
until (TS == 1) ; // wait for the press
count = count - 1;
PlaySound(SOUND_CLICK);
until (TS == 0) ; // wait for the release to keep
} // the RCX from counting too fast
}
turn_right() can also be a function. You can write this one
to make a right turn when the robot gets to the foil patch.
void turn_right()
{
// your statements go here
}
push_bottle() can also be a function. We will leave it empty
for now. You can fill it in next week or later today if you
finish early.
void push_bottle()
{
}
Grouping the #defines and any variables used in more than 1 task or function helps make your program more readable on our quest to become literate programmers. Now our program and main task will begin something like this.
// line3.nqc
// by your-name, your-teammate's-name
// A program to:
// start following a line after 2 taps on the touch sensor
// turn at the foil patch
// continue following the line
// stop when a bottle is found.
#define LEFT OUT_A
#define RIGHT OUT_C
#define TS SENSOR_1
#define LS SENSOR_2
#define TOO_DARK 44
#define TOO_BRIGHT 48
#define FOIL 60
int count,foil,bottle;
task main()
{
SetSensor(TS,SENSOR_TOUCH); // if setup here don't need to later
SetSensor(LS,SENSOR_LIGHT); // if setup here don't need to later
wait_start();
start follow_line;
foil = false;
start find_foil;
until (foil) ;
stop follow_line;
stop find_foil;
Off(LEFT+RIGHT);
turn_right();
start follow_line;
bottle = false;
start find_bottle_by_touch;
until (bottle) ;
stop follow_line;
stop find_bottle_by_touch;
Off(LEFT+RIGHT);
}
Last week we said that our simple line following program only worked from one side of the line. Be careful that when you make your right turn at the foil, you do not cross to the other side of the line!
Monday, January 23rd:
Review and improvement.
Today we will review what we know and try to use that knowledge to solve a few problems.
turn_right. Remember that to turn toward the right, the left wheel must go forward faster than the right wheel. One way is to simply stop the right wheel and turn the left one on for a little while. How long you leave the motor on, depends on your robot's battery charge and how much you want your robot to turn. You can experiment
// rturn.nqc
// A program to turn right a bit
#define LEFT OUT_A
#define RIGHT OUT_C
#define A_LITTLE_WHILE 20
task main()
{
OnFwd(LEFT);
Wait(A_LITTLE_WHILE);
Off(LEFT);
// another way to say the same thing would be:
// Fwd(LEFT);
// OnFor(LEFT,A_LITTLE_WHILE);
}
Try taking your robot with the line following program (now with the improved right turn) and see if it works
Last week we noticed many of the robots did not stop when they found the bottle. Watching the View screen with the ^ pointing at the touch sensor, we could see the touch sensor was not always pressed when the robot first hit the bottle. Again we need to make the robot more reliable. But this time, we need to redesign the "hardware." We found that adding a brick to stick out in front of the touch sensor worked. The brick was mounted on the touch sensor supports with a beam. (A beam is a brick with holes in it.) While we were modifying the touch sensor, the prongs were turned around so that we could push the bottle better.
With our improved touch sensor, it was time to push the bottle over the edge and then get back to the line. The result would be something like this. Because the floor is relatively far away from the light sensor, it reflects even less light than the black line. The light sensor reading when hanging over the edge for one robot was 33.
// line4.nqc
// by your-name, your-teammate's-name
// A program to:
// start following a line after 2 taps on the touch sensor
// turn at the foil patch
// continue following the line
// stop when a bottle is found
// push the bottle over the edge
// get back to following the line
// all the way back to the foil
#define LEFT OUT_A
#define RIGHT OUT_C
#define TS SENSOR_1
#define LS SENSOR_2
#define TOO_DARK 44
#define TOO_BRIGHT 48
#define FOIL 60
#define EDGE 36
int count,foil,bottle;
task main()
{
SetSensor(TS,SENSOR_TOUCH); // if setup here don't need to later
SetSensor(LS,SENSOR_LIGHT); // if setup here don't need to later
wait_start();
// follow the line and watch for the foil at the same time
start follow_line;
foil = false;
start find_foil;
until (foil) ;
stop follow_line;
stop find_foil;
Off(LEFT+RIGHT);
// now robot is over the foil
turn_right(); // get a little nearer line
// follow line and watch for bottle at the same time
start follow_line;
bottle = false;
start find_bottle_by_touch;
until (bottle) ;
stop follow_line;
stop find_bottle_by_touch;
Off(LEFT+RIGHT);
// now robot is touching bottle
push_bottle(); // new function for you to write
back_to_line(); // new function for you to write
// Now that robot is back on or just to the left of the line,
// (Remember your robot gets lost when it starts on the right of the line!)
// do the same things as before when you wanted to follow the line back
// to the foil.
// follow the line and watch for the foil at the same time
start follow_line;
foil = false;
start find_foil;
until (foil) ;
stop follow_line;
stop find_foil;
Off(LEFT+RIGHT);
// now robot is over the foil
}
// your other tasks and void functions from line3.nqc
// ...
// ...
void push_bottle()
{
// push forward...
// you could also push to the right, maybe faster
OnFwd(LEFT+RIGHT);
// stop at the edge with light sensor
until (LS < EDGE) ; // until find dark edge keep on going
// to find the edge by feel, wait until the bottle drops away instead
// until (TS == 0) ; // until touch sensor releases keep on going
Off(LEFT+RIGHT); // stop
}
void back_to_line()
{
// back up until near the oval line
OnRev(LEFT+RIGHT);
Wait(200); // This is just a guess.
// If you pushed your bottle by turning to the right you may be
// to the right of the line now. If you pushed straight then you
// may have to turn a little to the left.
Off(RIGHT); // Going backwards this will turn to the left
Wait(100); // This is just a guess.
Off(LEFT+RIGHT);
}
Monday, January 30th:
Robot to robot communication and building a proximity sensor.
The first step, as usual, is to get your computer and robot ready and then check the robot's battery level. Next, see George Miller about raising your yellow brick high enough that it can "see" over the touch sensor.
Our robots will work in pairs. One robot will send a message to the other robot that it is time to start. The second robot will wait for the message and then go forward and turn toward the line and follow the line to the foil. Prepare your robot to act in either role. You could download your send message program to Program Slot 1 and your receive program to Program Slot 2.
Cooperate with another team to test both of your programs.
The sending program will use a new function: SendMessage() and a new control statement: repeat (number-of-times) { }. Your teams have to agree on the same CODE for the message.
// sender.nqc
// by your-names
// A program to send a start message
#define CODE some-number-from-1-to-255
task main()
{
repeat (5) { // Send message several times to be sure
SendMessage(CODE); // it is received in a room crowded with
Wait(15); // other robots.
}
}
For today's practice session we will all use
#define CODE 3
The receiving program requires rewriting wait_start() so that the robot waits for the correct message instead of 2 taps. We will use another kind of comments,
/* multi-line
style
comments */
to temporarily remove the code that takes the robot from the
foil patch to the bottle and beyond. We need to write another
function: go_to_line(). This receiving program will use another
new built-in function Message().
// line5.nqc
// by your-name, your-teammate's-name
// A program to:
// start following a line after: the correct start CODE is received
// *** for testing, the robot will stop at this point ***
// turn at the foil patch
// continue following the line
// stop when a bottle is found
// push the bottle over the edge
// get back to following the line
// all the way back to the foil
#define LEFT OUT_A
#define RIGHT OUT_C
#define TS SENSOR_1
#define LS SENSOR_2
#define TOO_DARK 44
#define TOO_BRIGHT 48
#define FOIL 60
#define EDGE 36
#define CODE number-from-1-to-255
int mesg,foil,bottle; // change the name of the variable to watch
// while waiting from count to mesg
task main()
{
SetSensor(TS,SENSOR_TOUCH); // if setup here don't need to later
SetSensor(LS,SENSOR_LIGHT); // if setup here don't need to later
wait_start(); // wait for correct CODE
go_to_line(); // move robot over to, or near the line
// follow the line and watch for the foil at the same time
start follow_line;
foil = false;
start find_foil;
until (foil) ;
stop follow_line;
stop find_foil;
Off(LEFT+RIGHT);
/* temporarily finish here by "commenting out" the rest
// now robot is over the foil
turn_right(); // get a little nearer line
// follow line and watch for bottle at the same time
start follow_line;
bottle = false;
start find_bottle_by_touch;
until (bottle) ;
stop follow_line;
stop find_bottle_by_touch;
Off(LEFT+RIGHT);
// now robot is touching bottle
push_bottle(); // new function for you to write
back_to_line(); // new function for you to write
// Now that robot is back on or just to the left of the line,
// (Remember your robot gets lost when it starts on the right of the line!)
// do the same things as before when you wanted to follow the line back
// to the foil.
// follow the line and watch for the foil at the same time
start follow_line;
foil = false;
start find_foil;
until (foil) ;
stop follow_line;
stop find_foil;
Off(LEFT+RIGHT);
// now robot is over the foil
end of temporarily removed section */
}
void wait_start() // Version 2.0
{
ClearMessage(); // This line is very important in a room
// with lots of other robots!
mesg = 0; // this means we cannot use 0 as a valid CODE
SetUserDisplay(mesg,0); // display mesg for debugging
until (mesg == CODE) {
mesg = Message();
}
}
void go_to_line()
{
// Go forward a little.
// Turn left a little until the robot is on the line or
// just a bit to the left of the line.
}
// your other tasks and void functions from line4.nqc
// ...
// ...
Our second challenge today is to learn how to detect a pop bottle with a second light sensor instead of with the touch sensor. First, we will try a new NQC tool, the datalog. Then we need to learn another concept: responding to events.
The Datalog allows us to remember sensor values even after the program finishes running. It was Ok to use the little LCD to see one value at a time using the View Button or SetUserDisplay(). What if you want to see 50 values? The Datalog will remember them and later you can upload them to your computer for viewing. In this experiment we want to discover the reading on a light sensor, that is pointing forward and not down, when it gets close to a bottle. Remount your light sensor so it points the same way as the IR port. Then place a bottle in front of a robot, about 3 feet away. Set a yardstick or meterstick next to the robot. Now run a program like this and then upload the datalog and study the results. For this program we will try having the same function do different things by passing different "arguments" to the record() function. We will also try an "if" statement to decide whether an answer is the highest so far, and we will use a "local" variable called "answer" in the ping task. A local variable cannot be shared with other tasks. Our other variables were all "global", meaning they could be used by any part of the program.
// rangir.nqc
// by your-names
// A program to see how the light sensor value changes as a robot
// comes closer to the bottle. The light will be sent out from the
// IR port in tiny pulses. We will try 5 different messages to see
// if 0 bit, 1 bit, 2 bit, 4 bit or 8 bit pulses work best.
// This experiment requires human help. For each of the 5 test
// messages, the robot is placed 36 inches from the robot and then
// moved 6 inches closer at each double beep. After every placement
// tell the robot to continue by tapping the touch sensor. When the
// robot beeps at 6 inches from the bottle, it will also play a
// SOUND_FAST_UP to prompt you to move it back to 36 inches.
#define LEFT OUT_A
#define RIGHT OUT_C
#define TS SENSOR_1
// attach the light sensor for proximity testing to Input 3
#define PS SENSOR_3
// binary 00000000
#define NO_BIT 0
// binary 01010101
#define ONE_BIT 85
// binary 01010101
#define TWO_BIT 34
// binary 00001111
#define FOUR_BIT 15
// binary 11111111
#define EIGHT_BIT 255
#define FOREVER 0
int bits = 0; // the message sent from IR port to bounce off the bottle
int max_answer; // the highest answer received by the light sensor recently
int inches; // the distance from the bottle
task main ()
{
SetSensor(PS,SENSOR_LIGHT);
SetSensor(TS,SENSOR_TOUCH);
CreateDatalog(0); // erase any previous experiments
CreateDatalog(50); // 6 * 5 spaces, plus some extras
SetTxPower(TX_POWER_HI); // use high power for IR port
start ping;
record(NO_BIT);
record(ONE_BIT);
record(TWO_BIT);
record(FOUR_BIT);
record(EIGHT_BIT);
stop ping;
}
task ping()
{
int answer = 0; // This local variable is only used inside this task,
// max_answer is a global variable read and reset to 0
// in record(). We use the maximum reading because
// PS will bounce around a lot during a message: sometimes
// giving a reading when the IR light is on and sometimes
// when the IR light is off.
SetUserDisplay(max_answer,0);
until (FOREVER) {
SendMessage(bits);
answer = PS;
if (answer > max_answer) {
max_answer = answer;
}
}
}
void record (const int mesg)
{
AddToDatalog(mesg); // Mark the sections of this long Datalog.
bits = mesg;
inches = 36;
repeat (6) {
until (TS == 1) ; // do nothing until human taps touch sensor
max_answer = 0; // reset before each measurement
Wait(50); // measure for a generous 1/2 second
AddToDatalog(inches * 100 + max_answer);
PlaySound(SOUND_DOUBLE_BEEP); // tell human to move closer
inches = inches - 6; // move closer
}
PlaySound(SOUND_FAST_UP); // tell human to start over at 36 inches
}
From your datalog, find a good "threshold" for detecting that a bottle is near. Use the message code that gives the "earliest" warning. To be continued...
Wednesday, February 1st:
Moving the bar and find_bottle_by_light.
There are a few problems left for our robots to solve. First, the path clearing robot needs to find a way to move the bar at the entrance. The NQC for this is not hard but depends on your robot design. The robot design, in turn, depends on your strategy. Consider things like
Another problem we discovered was that our robots did not always find the line in the center of the T when starting from behind the cross line on the top of the T. The problem seemed to be that you had to be very careful to place your robot the same distance behind the line each time. To make sure that the robot would not start turning until after it crossed the line, we could alter go_to_line().
void go_to_line()
{
// Add two more lines to be sure you have crossed the start line
// before starting to turn.
until (LS < TOO_DARK) ; // until robot finds the crossing line
until (LS > TOO_BRIGHT) ; // until robot has completely crossed the line
// Go forward a little to get closer to the center.
// Turn left a little until the robot is on the line or
// just a bit to the left of the line.
}
Another problem is to write the function find_bottle_by_light. Continued from Monday...
Because we ran out of time you probably did not get to try this experiment yourself. Here is the datalog my robot produced.
0 3642 3037 2439 1844 1252 664 85 3639 3037 2439 1844 1251 664 34 3639 3038 2439 1843 1251 664 15 3637 3037 2439 1843 1251 663 255 3637 3037 2439 1844 1251 664Rearranged for readability
message 0 85 34 15 255 36in. 42 39 39 37 37 30in. 37 37 38 37 37 24in. 39 39 39 39 39 18in. 44 44 43 43 44 12in. 52 51 51 51 51 6in. 64 64 64 63 64We see that
Now write a program that heads toward the bottle and stops when the following event is detected: The light sensor reading is at or above the threshold you found in the previous experiment.
// prox.nqc
// by your-names
// A program to travel toward a bottle and stop when the proximity
// sensor detects the bottle.
// Similar to a program in Chapter 13 of Baum's book.
#define LEFT OUT_A
#define RIGHT OUT_C
#define PS SENSOR_3
#define PS_EVENT 1
#define PS_THRESHOLD 52
#define PS_MESSAGE 85
#define FOREVER 0
task main()
{
SetSensor(PS,SENSOR_LIGHT);
// Set our event to "monitor" or watch for, to be when the light sensor,
// used as a proximity sensor, reads above the threshold.
SetEvent(PS_EVENT,PS,EVENT_TYPE_HIGH);
SetUpperLimit(PS_EVENT,PS_THRESHOLD);
OnFwd(LEFT+RIGHT); // go forward
SetTxPower(TX_POWER_HI); // use high power for IR port
monitor (EVENT_MASK(PS_EVENT)) { // Start watching for a
until (FOREVER) { // proximity-sensor-high event.
SendMessage(PS_MESSAGE);
}
} catch { // When a proximity-sensor-high event is
PlaySound(SOUND_DOUBLE_BEEP); // noticed or caught, stop sending messages
Off(LEFT+RIGHT); // forever and play a tone and stop the
} // motors.
}
Now back to our mission problem: When you have cleared off the bottles on the line and your robot is back at the foil patch, your robot could do something like
void find_bottle_by_light()
{
// set proximity event
until (LS < TOO_DARK) { // stop at the tape on the other side of the oval
// go forward a little
// turn left 90 degrees
// turn back right 180 degrees
// turn left 90 degrees
}
// when event happens stop turning and go straight to bottle
// until touch sensor touches
}
You might want to practice a little more with your range-finder.
Alter your robot's program to turn slowly, instead of going
straight forward. Then start your robot near the bottle, but
pointing away from the bottle. Then see if it will stop
turning, and if it is then pointing at the bottle.
Monday, February 8th:
Warm up competition.
The competition will be a time trial. The total time will be
the sum of a team's 3 fastest runs. The team with the lowest
total time wins. Use 1 robot per team. The robot must
- Wait behind the line on the left side of the T, until the
message "3" is received.
- Go to the center of the T and start following the line.
- Continue on until you encounter a bottle placed on a straight
section of the loop.
- Push the bottle over the edge.
- Return to the foil and stop.
At the end of the competition you must turn in your program.
You can use your sender.nqc program as a practice starter.
Alternatively, this is the program we will use as a starter's
stopwatch. This program shows one use of the built in timers.
Notice that Timer(0) gives 1/10ths of a second. You are used to
Wait(1/100ths-of-a-second). FastTimer(0) will give you the
1/100ths of a second.
// starter.nqc
// A program to send a start message and act as a
// stopwatch.
#define STOP_BUTTON SENSOR_1
#define CODE 3
#define FOREVER 0
int time = 0;
task main()
{
SetSensor(STOP_BUTTON,SENSOR_TOUCH);
SetUserDisplay(time,1);
ClearTimer(0);
PlaySound(SOUND_FAST_UP);
repeat (5) { // Send message several times to be sure
SendMessage(CODE); // it is received in a room crowded with
Wait(15); // other robots.
}
until (STOP_BUTTON == 1) {
time = Timer(0);
}
until (FOREVER) ; // Keep the final time displayed.
}
10 Things to Remember
- Read the directions first: Before you start, be sure you know
where you are going. When you think you are done, read
those directions again.
- Make a plan: If you do not make a plan before you start programming
you will waste a lot of time with false starts. Worse, if you
don't have a plan, you will have no clue why, when or where your
robot started to go awry.
- Be prepared: You can waste a lot of time because you forgot
to check your robot's batteries and have no spares. Laptop
batteries will always run down if you forgot the power cord.
- Add comments to your programs: You will have many jobs in
the future. You will come to hope that the last person at
your next job wrote down what she did and why she did it.
To keep a job, you need to be able to tell others what you
have done and explain why and how you did it.
- Debugging takes thought: Thought before, in the form of
adding clues as to what your robot is doing, like playing
sounds and displaying variables and keeping a Datalog.
Thought during, in the form of not just changing a bunch of
things, but making a plan like, "If we change this
then we expect that will happen.
- Keep your workspace clean and organized: Time spent
searching for lost, small parts is wasted. Save your work
frequently. Use names for your programs that you will
remember. Make backups.
- Remember robotics is a real world engineering discipline:
This is not an exact science. No two runs of your robot
will be precisely the same.
- Learn to work with your team: The whole is greater than the
sum of its parts.
- Use the Internet: There are many people, like Dave Baum
who wrote NQC, who have posted many, many Web pages that
will be of help in your future studies.
- Have fun: Even when studying important subjects like
embedded systems and robotics, it's Ok to play with Legos.
Reported by John M. Miller M.D.
Revised February 4, 2006