Introduction
Multi-threaded programming is an important feature of modern software,
and Java provides a well-organized set of classes, interfaces, and methods
for creating threaded programs. Port/Device communication underlies many
of our popular user applications such as the software packages that download
images from digital cameras, upload mp3s to our iPods, and transfer data
from webcams to display on a screen. Java does not typically provide
classes for directly interfacing with serial or USB ports. It does, however,
provide a scheme for treating C code as if it were wrapped inside a Java
class called the Java Native Interface (JNI). Wireless
networking is
also an important basis upon which many of our applications operate.
Protocols are rules specifying, among other things, the exact format
of messages allowed to be passed between units in a network (wireless
or otherwise).
Project Overview
In this project you will gain experience in all four areas by designing
a system that sends formatted infrared signals to two Lego RCX units
to make them play short tunes based on music data stored in two plain
text files. The top-level portion of your system will be written in Java,
and will contain two threads (besides the main thread) to communicate
with the two RCXs. You will communicate with the RCXs using a serial
Communicator class that relies on C code targeted for the Windows XP
platform. The Java Native Interface (JNI) will be used to set up a link
between this code and the Java Virtual Machine (JVM). The two threads
must be coordinated so that they send the correct command to each RCX
(and receive the correct acknowledgment) so that there are no serious
delays in the music being performed. You’ll be interleaving invocations
of the serial class’s methods to do this.
In the interest of time,
I’ve given you all of the C code and the directions for making
the dynamic link library (DLL) that Java will need to have available
for its JNI. You are free to modify it and recompile it in Visual Studio
if you think you’d rather enforce threadsafety at the C level of
this project instead of relying on Java’s higher-level synchronized construct.
Here are the files you need:
I’m also summarizing the protocol that the software (firmware)
on the RCX expects so that you can quickly write a method to translate
the notes that I’ll be expecting your program to tell the RCXs
to play into bytecode sequences that the RCXs can understand.
Step 1: Create DLL for Java to use Serial
Ports
The JNI requires that the C/C++ code be compiled as a dynamic link library.
This is so that the code can be loaded into memory on demand by the operating
system into memory whenever the JVM loads a class whose methods are based
on “foreign code” like C/C++. To do this in the Windows XP
environment that the project will run on, you’ll need to use Microsoft
Visual Studio on the PCs in the Software Engineering Lab (M158). Once
you’ve created the DLL, you can also copy that file to the PCs
in the Intelligent Systems Lab (M156). The computers in M156 do not have
large enough hard drives to store Visual Studio, so the actual creation
of the DLLs is not possible in that lab.
The file communications.cpp
contains all of the code for basic serial port access operations from
within Windows. It is in Appendix A of this handout, and you can also
download it from the course website. If you look at the code you should
see that it looks just like any regular C code except that the procedures
all have the JNIEXPORT and JNICALL modifiers in their declarations. JNIEXPORT
tells Visual Studio to make a procedure visible from the compiled DLL.
That is, the compiler will consider Java code that calls the procedure
as “safe,” and will allow the Java code to use the C code.
The JNIEXPORT modifier allows a programmer to bundle “internal” helper
procedures (that one wouldn’t really want the Java code to accidentally
be able to use for fear that the access could corrupt privileged data)
with “public” procedures that form the application programmer
interface (API) of the DLL.
Different languages use different bit-level
representations for various data types. Some might treat characters as
7-bit signed integers and ignore the sign bit while others treat them
as 8-bit unsigned integers and pay close attention to that eighth bit.
When we combine compiled code from two languages, we need to be able
to tell the compilers whether any translation needs to be done to data
being passed from one language as procedure parameters or a procedure
return value to code in another language. The JNICALL modifier tells
the compiler when to set up machine code that will translate C data type
representations into Java data type representations, and vice version.
If you look closely at the parameter lists of the procedures in communications.cpp,
you’ll notice that each has an initial parameter called “env” that
is a pointer to a data structure of type JNIEnv. Each also has a second
parameter called “obj” of type jobject. The parameter is
how C code is given access to the information in the slots on an object
in which it functions as a method. The first parameter is how information
about the state of the JVM is passed to C code.
The following directions will show you how to create a DLL from it using
Visual Studio. You’ll need to use both the communications.cpp file
in Appendix A and its header file in Appendix B.
- Open Visual Studio
- Select New Project from the start page.
- Choose Win32 Project
and name it “communications”
- On the next menu, click
Application Settings on the left Side.
- Select the options "DLL" and "Empty
Project"
- Click Finish
- In the Projects bar on the left side,
right click on Source Files
- Choose "Add Existing..." and
find "communications.cpp"
- In the Projects bar on the left
side, right click on Header Files
- Choose "Add Existing..." and
find "communications.h"
- Right Click on the "communications" project
in the Projects bar
- Select "Properties"
- In the Properties
Menu, select the folder C/C++
- Click on Additional Include Directories
and Add
C:/%Your Java Home Directory%/include/
C:/%Your Java
Home Directory%/include/win32/
- Exit the menu
- Click on the Build Menu on the top menu bar
- Select Build All
At this point, you’re done building the DLL. All you need now is
to copy the resultant .dll file it into the directory of the java program
that you are using it with.
Step 2: Write Java Code that Uses the JNI
to use the DLL
Once the native C code has been turned into a DLL, you’ll need
to write Java code that produces a “wrapper” class whose
methods make use of the DLL’s exported procedures. In appendix
C I’ve given you a complete Java file that does this for this project.
The file defines a class called “Communicator” with the public
methods “Open,” “Close,” “setupPort,””setPortTimeout,” “getReply,” and “writePacket.” If
you examine the code you’ll see that these methods rely on private
methods declared with the native modifier. This tells the Java compiler
to expect to look for compiled C code to define those methods, and the “loadLibrary” method
tells the Java compiler to look for the compiled code in a file named “communications.dll”.
To
use this code and the .DLL file correctly, you’ll need to place
them in the same directory, and pass Communicator as an additional argument
to javac when you are compiling the Java code that you’ll be writing
for the project.
I’m assuming that most of the Communicator methods
are self-explanatory. The “set” methods need some extra background.
You use “setupPort” to set the baud rate of the serial port.
You should set it to 2400 in this project. You use “setPortTimeout” to
tell the serial IR Tower how many milliseconds to wait before concluding
that there is no more IR data to read from the RCX. A good number here
would be anything between 80 and 300, depending on how bright the lighting
is in the room you’re working with. You might need to experiment
with this number if you don’t get the whole response back from
an RCX.
We send a message to the RCX by storing the bytes of the message
into an array and then passing it to the “writePacket” method.
This method returns true if all bytes of the message were successfully
transmitted out of the IR tower. The “getReply” method is
what we use to see if an RCX has sent back an acknowledgment message.
It returns returns the single next byte from the buffer on the tower.
You'll need to call it in a loop until all the bytes have been read.
You should be storing this sequence of bytes into an array on your own.
The .getReply method returns a 0 when there are no more bytes left to
read, but be careful here. Remember that the first three bytes of all
of our messages will be 55 FF 00 in hexadecimal. That third byte is a
bona fide 0, which will be dutifully returned by the .getReply() method.
Instead, we need to modify the "stop" criteria to look for
0's being returned *in a row*. If we get one 0, we need to see if the
next call returns a non-zero value, in which case the 0 is actually part
of the reply. If another two 0’s are returned, we then definitely
know (for our project, anyway) that the first zero was "end of message." This
is a case where interested students might want to modify the C++ code
to return NIL when there is nothing in the buffer to read.
Step 3: Write Java Code that Communicates
with RCXs
The yellow RCX units that you’ll be working with each consists
of a lot of hardware (infrared port, LCDs, gray sensor ports, black motor
ports, a CPU, RAM, etc) and require some software to manage all of it.
We call the software “firmware,” and it is a cross between
an operating system and a virtual machine. You’ll need to make
sure that your RCX units have no pre-existing firmware in memory before
proceeding with the project. To do this, just take out one battery and
press the green on/off button. That will clear any software left in RAM,
and allow you to download new Mnet.lgo firmware into the RCX. You should
visit either Dr. Klassner or Drew Jacobs to get the correct firmware
installed on your RCX, and to have the correct RCX ID stored into your
RCX. Make sure that the batteries are never taken out longer than for
a minute, otherwise you’ll lose the firmware from memory and have
to start again. This can be done in the Software Engineering Lab or the
Intelligent Systems Lab.
The RCX firmware that we’re using executes
commands in much the same way that the JVM or a CPU does. It loads an
opcode byte (an instruction) into a command register, loads in any arguments
(operands) that the instruction requires, and then executes the instruction.
These instructions can either be stored on board the RCX as a program
in memory alongside the firmware, or they can be sent individually to
the RCX via infrared signaling. In this project we’ll be sending
them from the desktop PC through the infrared tower.
Any signal that
we want the RCX to respond to must conform to the firmware’s infrared
(IR) protocol. All messages must have the format:
55 FF 00 OPCODE ~OPCODE
ARG1 ~ARG1 ... ARGN ~ARGN SENDER ~SENDER TARGET ~TARGET SUM ~SUM
This
means that the first three bytes of any message must contain the hexadecimal
values 55, FF, and 00 (85, 255, and 0 in decimal). The next two bytes
in the message must be the code for the command we want to execute, followed
by the bitwise complement of that bytecode. The next set of bytes consists
of the arguments that the command needs, with each byte of the arguments
followed by its bitwise complement. After the last argument byte we place
the 1-byte identifier of the machine that is sending the data (01 in
our case) followed by the complement of that value. We then place the
1-byte identifier of the machine that the message is intended for (your
RCX’s id in this case), followed by the bitwise complement of that
value. In the final two bytes of the message we place the checksum of
the message and the checksum’s bitwise complement. The checksum
is the low-order 8 bits of the sum of all the (uncomplemented) bytes
of the opcode, arguments, and sender and target.
The RCX continuously
polls its IR port register to see if a new opcode has been received.
If the same opcode (even if the arguments are different!) must be sent
to an RCX two or more times in a row, then the RCX needs some indication
that the new signal is in fact a new message and not just an old copy
of an opcode already invoked earlier. This is done by alternating the
4th bit (bit number 3 if we’re counting with the low order bit
labeled as 0) from a 0 to a 1 to a 0 to a 1, etc. each time the command
is repeated.
When the RCX receives a message, it executes the command
and sends back an acknowledgment message. The acknowledgement always
starts with an exact duplicate of the original message, followed by a
response byte, followed by its complement, followed by the “sender” and “target” fields
and a new checksum and checksum complement for the response byte. You
can thus check to see whether an RCX received your message or not, and
whether it was the right one. In this project you’ll only need
to know one opcode: PLAY-TONE. This opcode has hexadecimal value 23 or
2B, depending on whether or not the toggle bit is set. The opcode must
have three bytes of operands. The first two bytes (making up the first
argument) are the frequency of the tone, and the third byte (the second
argument) is the duration of the tone in hundredths of a second.
The
protocol and the RCX are “little-endian.” This means that
values that are spread out over more than one byte (for example, the
frequency argument value) must be arranged so that the low bits come
first and the high bits come later. Therefore, when you put together
the command package, make sure that the low-order byte of the frequency
value is placed in the message array before the high-order byte.
If you
send up to 5 “PLAY-TONE” requests to an RCX, these will be
buffered so that the RCX can play one note while receiving more requests.
However, if you send more than 5 notes before the RCX has a chance to
play the first, then the buffer will “overflow” and the new
notes (beyond the first five) will not be stored, but will be forgotten
and not played. It is important for you to keep the buffer from overflowing
by making sure that your threads in your Java program don’t send
too many messages too quickly to the pokey RCXs! You should consider
using .yield and .sleep thread methods in your Java code for this.
There
is no extra RCX response byte for this message, beyond just the echo.
Step 4: Project I/O Specifications
You need to write your Java code so that it first opens two text files
that will contain the notes for the two RCXs. These will have the names “song1.txt” and “song2.txt” and
will be in the same directory as your program. You are free to read in
the files one line at a time, or to read in each whole file into an array
for faster transmission, if you think you need it. Each file’s
line will represent a note and will have the following format. There
will be two integers. The first will be a number representing the note’s
frequency in Hertz. The second will be a number representing the duration
of the note in hundredths of a second. You must pass this information
to the appropriate RCX. There is no “end of file” code, and
the files are not required to be of the same length. You’ll need
to check whether there is any more data left in the files each time you
read from them.
Step 5: Write Java Code the Coordinates
Threads
The final “step” of this project is to write Java code that
spawns two threads to communicate with your two RCXs to make them appear
to play their music simultaneously. Be sure that you don’t try
everything at once here. First make sure that you can send properly formatted
data to the RCX, and verify that you got a correct response. If you don’t
get a correct response, you should assume the note was lost and you should
try sending the note request again.. Then, get one thread to communicate
with one RCX. Finally, try getting two threads to perform correctly.
Be careful that the Communicator object’s methods are used by only
one thread at a time! Also, don’t let one thread close the port
before the other is finished using it! You shouldn’t need to change
the C code to enforce thread safety, but I won’t stop you if you
want to.
You should make up some short tunes in test files to test your code
on. Here are frequency codes that correspond to various notes:
| A |
A# |
B |
C |
C# |
D |
D# |
E |
F |
F# |
G |
G# |
| 440 |
466 |
494 |
523 |
554 |
587 |
622 |
659 |
698 |
740 |
784 |
831 |
Evaluation and What to Hand In
Your program will be evaluated on the basis of how well the two songs
are played without losing too many notes. If you only get one thread
to work, then you can expect about a 75-80 on the project. Note skipping
will cost you, as well.
You should hand
in:
- a separate (typed, please!) document of no more than 2 pages describing
the overall program design, a description of “how it works,” and
the design tradeoffs considered and made. Also describe possible improvements
and extensions to your system (and sketch how they might be made).
Be sure to include an explanation of why your threads might run faster
or slower depending on whether the music files are loaded in all at
once or if they are read in a line at a time and each line sent to
the RCX before the next line is read in.
- a program listing (source code) containing inline commenting for
the Java code. If you made no changes to the C code, then there is
no need to hand that in.
- a separate description (no more than 1 typewritten page, please)
of the tests you ran on your program to convince yourself that it is
indeed correct. Also describe any cases for which your program is known
not to work correctly.
Hints
- In Java, remember that the bitwise complement operator is the tilde
(~).
- When you are using the .Open method, you need to pass in the string "\\\\.\\COM1" to
open up serial port 1 or "\\\\.\\COM2" to open up serial
port 2.
- Your actual Java program should have a MusicPlayer class that will
extend the Thread class. It is inside the .run method of this class
that you'll be placing all the code for reading from a plain text
song file, translating the notes into byte sequences to send to the
RCX, sending the byte array to the RCX, and getting the RCX's response.
The constructor for this class should take three input parameters:
the serial communicator, the ID of the RCX that the given thread will
need to communicate with, and the name of the song file the thread
will need to read from.
- The
main method of your program should be where the single instance of
the Communicator class is instantiated (i.e. where you call "new Communicator"),
and where you create the two threads and call their .start() methods.
It will be wise to make the main method (which is a thread itself)
be responsible for opening and closing the Communicator object. Be
careful to make sure that the serial port is closed when your program
finally exits, otherwise the Windows OS will think that the port is
still in use and won't let your program open the port again if you
run it again in the same login session. You'll have to log out or even
restart the computer to get the serial port back!
- See Andrew Chang’s Independent
Study to find out what the bytecode is that sets the ID of the RCX if
you don’t have time to see me.
Acknowledgements
This project was created by Frank Klassner.
© 2001, 2004 by Scott Anderson, Frank Klassner, Pam Lawhead,
and Myles McNally. This work is supported by NSF grants 0088884 and 0306096. Permission
to use, copy, adapt and modify these materials for instructional purposes is granted.
These materials can be obtained from our web site
www.mcs.alma.edu/LMICSE. If you have suggestions
for improvement, please contact us via the web site; we would really appreciate
it. This file was last modified on
June 7, 2005.