APRO Does Lego

We receive “how I use APRO” messages from our customers from time to time, and we’re amazed at the diversity of projects cited. Everyone knows APRO can be used to control modems, download/upload files, record voice messages, send/receive faxes and so on – because our marketing material says so.

It might be surprising, however, that a large number of people are using APRO for unusual things like measuring bacteria content in milk, weighing chickens on a conveyor belt, measuring/counting fish as they swim past sensors, controlling mechanical screwdrivers on the space shuttle, editing sound for major motion pictures, and reading telemetry from missile guidance systems.

The truth is, the low level components in APRO are designed to be flexible enough to let you do just about anything that can be controlled or read from a serial port. That means bar code readers, electronic scales, LCD and LED displays, laboratory equipment – you name it. It’s simply a matter of coming up with (or figuring out) what needs to be sent and/or received through the serial port.

Reading about usual applications makes TurboPower’s communications engineers a bit envious at times, since we mainly have to stick to regular comports and modems (our janitor objected to housing chickens or fish here, and milk with a lot of bacteria stinks).

Enter Lego Mindstorms.

Lego Mindstorms are robotic “toys” (for kids of all ages <g>) that combine the usual Lego building blocks with sensors, motors, and a central controller (called the RCX, or “brick”); which is typically controlled or programmed through an infrared port.

Lego’s Mindstorm website is here: http://www.legomindstorms.com/

Complete Mindstorm packages, such as the Robotics Invention System, come with an infrared tower that connects to the serial port of a PC along with software to program the RCX. Unfortunately, the supplied software is geared towards non-programmers and is quite limited (although it looks nice).

The ActiveX component (SPIRIT.OCX) that the Lego software uses to communicate with the RCX is documented, and it’s a trivial matter to use it with Delphi. Several applications using the ActiveX are available; such as the RCX Command Center. http://www.cs.uu.nl/people/markov/lego/ (the RCX Command Center is written in Delphi, incidentally). The RCX Command Center is no longer maintained, but there is still quite a bit of useful information on their website.

Using SPIRIT.OCX has limitations though. The ActiveX must be installed on the target machine and the ActiveX is not officially redistributable. I personally ran into this problem when I tried to use the RCX Command Center to write some code on a machine other than the one my Lego software was installed on.

Additionally, SPIRIT.OCX only supports comports 1-4; so you can’t use it to simultaneously program an army of Lego robots with your 16-port serial board.

This is where APRO can help. The Mindstorm IR tower connects to a standard serial port on your computer, which makes it a snap once you figure out what to send the RCX.

A few starting places to get this information are:

·RCX Internals http://graphics.stanford.edu/~kekoa/rcx/

·Lego Mindstorms Internals http://www.crynwr.com/lego-robotics/

·MIT’s site http://fredm.www.media.mit.edu/people/fredm/mindstorms/index.html

Let’s walk through a few simple things to get you started.

First off, the communications parameters for talking to the IR tower are a little unusual. Set the TApdComPort as follows:

·Baudrate: 2400 (4800 might work, but is less reliable)

·Databits: 8

·Parity: pOdd

·Stopbits: 1

 

Finding an installed IR tower is something you might want (or need) to do to relieve the end user from figuring out which port it’s installed on. This is relatively simple to do, since the tower has the RTS and CTS lines tied together. Simply cycle through the available comports and test whether CTS tracks with RTS.

Delphi code:

uses AdSelCom;

procedure TStormForm.FindTower(Sender: TObject);

var

 I : Integer;

 SaveRTS : Boolean;

begin

 for I := 1 to 6 do begin

   if IsPortAvailable(I) then begin

     { See if the port is there }

     ApdComPort1.ComNumber := I;

     ApdComPort1.Open := True;

     SaveRTS := ApdComPort1.RTS;

     { Does CTS track RTS? }

     ApdComPort1.RTS := True;

     if ApdComPort1.CTS <> ApdComPort1.RTS then begin

       { Doesn't track, restore port and try again }

       ApdComPort1.RTS := SaveRTS;

       ApdComPort1.Open := False;

       Continue;

     end;

     { Check again, does CTS track RTS? }

     ApdComPort1.RTS := False;

     if ApdComPort1.CTS <> ApdComPort1.RTS then begin

       { Doesn't track, restore port and try again }

       ApdComPort1.RTS := SaveRTS;

       ApdComPort1.Open := False;

       Continue;

     end;

     { Looks like we found it, restore port }

     TowerPort := I;

     ApdComPort1.RTS := SaveRTS;

     ApdComPort1.Open := False;

   end;

 end;

 if TowerPort = 0 then

   raise Exception.Create('IR Tower not found');

 Label1.Caption := Format('IR Tower found on com%d', [TowerPort]);

end;

C++Builder code:

void __fastcall TForm1::FindTower(TObject *Sender)

{

 bool SaveRTS;

 for (int I=1; I<=6; I++) {

   if (IsPortAvailable(I)) {

     // See if the port is there

     ApdComPort1->ComNumber = I;

     ApdComPort1->Open = true;

     SaveRTS = ApdComPort1->RTS;

     // Does CTS track RTS?

     ApdComPort1->RTS = true;

     if (ApdComPort1->CTS != ApdComPort1->RTS) {

       // Doesn't track, restore port and try again

       ApdComPort1->RTS = SaveRTS;

       ApdComPort1->Open = false;

       continue;

     }

     // Check again, does CTS track RTS?

     ApdComPort1->RTS = false;

     if (ApdComPort1->CTS != ApdComPort1->RTS) {

       // Doesn't track, restore port and try again

       ApdComPort1->RTS = SaveRTS;

       ApdComPort1->Open = false;

       continue;

     }

     // Looks like we found it, restore port

     TowerPort = I;

     ApdComPort1->RTS = SaveRTS;

     ApdComPort1->Open = false;

   }

 }

 if (TowerPort == 0) {

   throw ("IR Tower not found");

 }

 Label1->Caption = "IR Tower found on com" + IntToStr(TowerPort);

}

Once you’ve found the tower, you can send commands to the RCX (assuming the RCX is in range and powered up). A short note about the format of the command would be helpful here (more detailed information about the commands can be found on the websites mentioned above).

 

Every command has a three byte header consisting of $55$FF$00.

 

Following the header is the command itself, along with any information needed to support the command. The bytes in the command body are alternated with notted bytes (presumably a form of error correction). In other words, every byte in the command is expanded into two bytes; with the first byte being the command byte and the second byte being not(command byte).

 

Finally, the last two bytes are a checksum of the command bytes followed by – you guessed it – that value notted.

 

As an example, a command used to determine if the RCX is present and powered up (essentially similar to the Internet’s “ping”) is $10.

 

The complete, properly formatted message is as follows:

 

 $55$ff$00$10$ef$10$ef

 

Notice the checksum is the same as the command since the command was only a byte long. This forces a special rule for the message format – identical commands cannot be immediately repeated. If it’s necessary to do so, alternate setting the $08 bit in subsequent commands. So three pings becomes (header, not bytes and checksum not shown for clarity):

 

 $10$18$10

 

Example code to send a single ping is pretty straightforward:

 

Delphi code:

 

procedure TStormForm.PingRCX(Sender: TObject);

var

 S : string;

begin

 { Find the tower if necessary }

 if TowerPort = 0 then FindTower(Self);

 { Prepare string for ping command }

 S := #$55#$ff#$00#$10#$ef#$10#$ef;

 Memo1.Lines.Add('Sending Ping...');

 { Send command }

 ApdComPort1.Open := True;

 ApdComPort1.PutString(S);

end;

C++Builder code:

 

void __fastcall TForm1::PingRCX(TObject *Sender)

{

 // Find the tower if necessary

 if (TowerPort == 0) {

   FindTower(this);

 }

 // Prepare string for ping command

 String S = "\x55\xFF\x00\x10\xEF\x10\xEF";

 Memo1->Lines->Add("Sending Ping...");

 // Send command

 ApdComPort1->Open = true;

 ApdComPort1->PutString(S);

}

Receiving the response is equally simple (in this case, we just show what comes back in a memo):

 

Delphi code:

 

procedure TStormForm.ApdComPort1TriggerAvail(CP: TObject; Count: Word);

var

 I : Integer;

 S : string;

begin

 for I := 1 to Count do begin

   S := S + Format('[%0.2x]', [Byte(ApdComPort1.GetChar)]);

 end;

 Memo1.Lines.Add(S);

end;

C++Builder code:

 

void __fastcall TForm1::ApdComPort1TriggerAvail(TObject *CP, WORD Count)

{

 String S;

 for (Word I=0; I<Count; I++) {

   Char C = ApdComPort1->GetChar();

   S = S + "[" + IntToHex(C, 2) + "]";

 }

  Memo1->Lines->Add(S);

}

The IR tower echos everything you send – and the RCX gives a response related to the message sent (it reverses the bytes in the command body). This is what you should see in the memo (the bytes may be grouped differently depending on how they were received by APRO).

Sending Ping...

[55][FF][00][10][EF][10][EF]

[55][FF][00][EF][10][EF][10]

The first line of data above is the IR tower echo, the second line is the reply from the RCX.

Another simple command to try is:

Delphi code:

procedure TStormForm.PlaySound(Sender: TObject);

var

 S : string;

begin

 { Find the tower if necessary }

 if TowerPort = 0 then FindTower(Self);

 { Prepare string for play sound command }

 S := #$55#$ff#$00#$51#$ae#$03#$fc#$54#$ab;

 Memo1.Lines.Add('Play Sound...');

 { Send command }

 ApdComPort1.Open := True;

 ApdComPort1.PutString(S);

end;

C++Builder code:

void __fastcall TForm1::PlaySound(TObject *Sender)

{

 // Find the tower if necessary

 if (TowerPort == 0) {

  FindTower(this);

 }

 // Prepare string for play sound command

 String S = "\x55\xFF\x00\x51\xAE\x03\FC\x54\xAB";

 Memo1->Lines->Add("Play Sound...");

 // Send command

 ApdComPort1->Open = true;

 ApdComPort1->PutString(S);

}

This causes the RCX to play a series of tones. Your memo should show something like this following the command:

 

Play Sound...

[55][FF][00][51][AE][03][FC][54][AB]

[55][FF][00][AE][51][AE][51]

 

Notice the RCX only responds with the command byte, not the additional parameter passed (the type of sound to play).

 

Finally, shutting down the RCX:

 

Delphi code:

procedure TStormForm.PowerOff(Sender: TObject);

var

 S : string;

begin

 { Find the tower if necessary }

 if TowerPort = 0 then FindTower(Self);

 { Prepare string for power off command }

 S := #$55#$ff#$00#$60#$9f#$60#$9f;

 Memo1.Lines.Add('Shutting down...');

 { Send command }

 ApdComPort1.Open := True;

 ApdComPort1.PutString(S);

end;

C++Builder code:

void __fastcall TForm1::PowerOff(TObject *Sender)

{

 // Find the tower if necessary

 if (TowerPort == 0) {

  FindTower(this);

 }

 // Prepare string for power off command

 String S = "\x55\xFF\x00\x60\x9F\x60\x9F";

 Memo1->Lines->Add("Shutting down...");

 // Send command

 ApdComPort1->Open = true;

 ApdComPort1->PutString(S);

}

Your memo should show something like this following the command:

 

Shutting down...

[55][FF][00][60][9F][60][9F]

[55][FF][00][9F][60][9F][60]