Lab 1: Trainspotting

In this assignment you will write a program to control two trains. The purpose
is to write the program so that the trains move relatively independently of each
other, but without colliding. Instead of a real railway, we have a simulator,
where one can control the speeds of trains and change rail switches. The
simulator gives a signal when a train passes sensors suitably placed on the
tracks. It is part of the assignment to decide where sensors should be placed to
give sufficient information for your program. The map without sensors looks as
follows.

Choice of programming language

In this assignment you must use Java.

Important

In order to make sure you are using the right version of Java and train
simulator, you should run

setup_course tda381

on Chalmers GNU/Linux machines.

Assignment

Your assignment is to write a program that controls the trains. The trains
should run simultaneously between the two stations (note that each station has
two tracks). It is important that the trains are able to move at the same time,
and be as independent from each other as possible, without crashing. At the
stations the trains should stop and then wait 1 to 2 seconds. The trains should
run forever between the stations.

In the beginning there are no sensors on the tracks. You must find where it is
suitable to place sensors so that the control program gets the necessary
information.

The program that you deliver should work as follows. Each train should be
controlled by an independent train program, which runs in its own Java
thread. Train programs cannot communicate with outside world by anything else
than acquiring and releasing semaphores. In particular, no shared variables
should be read or changed by the train programs. Also, both trains must use the
same 'train program', and it is not allowed to make the behaviour of a train
depend on its train id. For example, the track that a train chooses should not
depend on its train id.

Below is a list of more detailed requirements.

Requirements

Waiting at stations. The trains must wait 1-2 seconds at each
station after coming to a full stop. Avoid randomness. You can use a formula
such as: 1000 + (20 * |train_speed|)

Maximum train speed. You need to find out the maximum train speed maxspeed
that is usable with your particular solution. This must be at least 15.

No minimal speed. There should be no assumption about minimal train
speed. All speeds in the range [1,maxspeed] will be tried.

No excessive semaphores. Solutions that use 10 or more semaphores will
be rejected.

No polling. Your solutions must not use busy-waiting-loops or their
close relative, polling.

Dynamic behaviour. You must not statically encode how the trains choose
track at the switches (for example: train 1 always takes the upper path in the
middle, while train 2 always takes the lower; same for stations). You must let
the trains have one track as default (for example: the track with shortest
distance). You must select the other track if the default track is already
occupied by the other train. Note that you should not try to find a solution
which works for any track layout or any number of trains, as this will be too
complicated for all of us! It is enough to solve the problem for the given track
layout.

No map modifications. You may not alter the map in any way, other than by
adding sensors.

No randomization. You may not use randomization in any way that affects the
simulation, as this makes testing more difficult.

Two trains – two threads. When the system is running there should be only
two processes (threads): one for each train. This is in addition to the main
thread and the thread that handles communication with the simulator. This
latter thread is created in the interface code we provide you with.

Two trains – one implementation. The solution must use the same
implementation for both trains. The only thing separating the processes of the
different trains is the train id, starting position, and speed. The fact that
the two trains start in different positions means that it is permitted (and
necessary) to have different initial behaviour for the two processes, but
after that the behaviour of the processes must not be dependent on which
train it is acting for.

Use binary semaphores. You must use the provided binary semaphore for
mutual exclusion. You will have the opportunity to use other synchronisation
mechanisms in the other assignments.

Trains mind their own business. Trains should operate independently. A
train should not make plans/decisions for the other train. Further, a train
should not make plans/decisions for itself based on the information (speed,
position, direction) of the other one.

Good train flow. The parallel section in the middle of the map must allow
a fast train to overtake a slow train going in the same direction.

Documentation. In addition to working code you need to provide a convincing
argument why your solution is correct. You must document your solution with an
ASCII text file. Give a high-level description of your program and then go
into detail about all pieces of code that are essential for correctness. The
documentation should include everything needed for easy understanding of the
solution. In particular we demand that your documentation contains discussion
of:

Placement of the sensors

Choice of critical sections

Maximum train speed and the reason for it

How you tested your solution

Code quality. Reasonable code quality is expected; do not complain if your
assignment is rejected because we think your program is ugly (magic numbers,
poor use of types, cut-and-paste coding style) – even if it works.

Submission. When you submit your solution, you must provide the following
files:

Lab1.java (your program to control the trains)

Lab1.map (which indicates the placement of your sensors)

documentation.txt (ASCII file containing your documentation)

Train simulator

The simulator is called tsim and with this program you can even modify the train
map. You can run tsim as follows:

tsim Make a new, empty map, of default size.

tsim filename Read in an old map from file filename.

tsim --speed 50 filenameRead in an old map from file filename, and also
set the simulation speed to 50. This value is the number of milliseconds that
tsim waits between updates of its state (i.e moving the trains). 100 ms is the
default, so 50 makes the simulation go at double speed.

tsim --helpSee all options of the simulator.

tsim as a map editor

You need to get your own copy of the map in order to be able to add the
sensors. The map you will be working on is that shown above.

Then you can start to modify the map with tsim Lab1.map. The figure below shows
how to use tsim. You should only select the sensor button in the tools window
(the small T-like symbol) and then use the mouse to place sensors on the track.

tsim as a simulator

You may start tsim and interact with it using commands on standard input. Try
for example the commands

SetSpeed 120
SetSwitch 177 RightSwitch

and see what happens. (The second command is necessary for the train to survive
the first switch which initially is set in the wrong direction).
We provide the Java library to handle the communication with tsim, which lets you change the speed of the trains and the state of the switches in a more convenient way (see next section).

Here are some things to note about the simulator:

When you give a new speed for a train, it takes a while for the train to
increase or decrease from its old to its new speed. This means that the
train's braking distance puts limits on how fast they could drive.

The trains can not turn around, but can be given a negative speed to travel
backwards.

You are not able to give a speed which implies that a train changes
direction. To change direction the train must first be stopped (by giving it
speed 0). There is no signal from the simulator to tell you when the train has
stopped, so you will need to wait a suitable time before changing direction.

You must be careful when you change the switches. If a train comes to a switch
which is in the wrong position, then the train will derail. See the figure
below.

Program/Simulator interaction

The simulator and your program run as separate OS processes. They use pipes to
communicate.
The Main class provided in the skeleton code takes care of joining together the pipes of your solution in Java with the tsim simulator.

All commands between the your program and the simulator are sent
line-by-line as text strings, as you saw above. To aid you in your
assignment we provide you with a Java library (TSim) that implements
the protocol tsim is using. You must use this Java library. The
package provides you with an interface to control the behaviour of the
trains and switches.
You can view the JavaDoc documentation of this library
here.

Usage

You cannot create instances of TSimInterface using its constructor, since it
is private. Instead you use the static method getInstance() that creates an
instance of the class if that has not already been done (singleton pattern).

TSimInterface tsi = TSimInterface.getInstance() ;

Interesting classes

TSimInterface is the class used to control the trains. It contains methods
to control the speed of the train and to switch the positions of the
switches. All these methods throw a CommandException if some tsim-related
error occurs. In addition there is a method to turn on debugging, which
displays the communication between your program and tsim. Read the Javadoc
documentation of this class carefully!

SensorEvent represents the event of a train passing over a sensor. Every time
a train passes over a sensor first an ACTIVE event is created followed by an
INACTIVE event when the train leaves the sensor. This class contains the
status of the sensor, the position of the sensor and the identity of the train
causing the event.

CommandException represents a failure of one of the methods of
TSimInterface. Information on the error is stored in the exception and can
be acquired by the getMessage method. You should never catch an exception
leaving the exception handler empty! For your own and our best always print
the error message contained in the exception object. Otherwise, your program
will be much harder to debug.

Downloads

Getting started

You should implement your solution in the Lab1.java file.
We provide a Main class which takes care of loading the simulator and connecting it to your solution. You shouldn't need to change this file.
This entry point expects at least 3 command line arguments:

map file

speed of train 1

speed of train 2

simulator speed [optional]

Using the command line

To build your solution, you can simply run the following command:

makeall

from the root directory of the project.
This will place the class files in the bin folder.

You can then run your solution with:

java -cp bin Main Lab1.map515

Using Eclipse

You can also open the files as an Eclipse project:

Extract the trainspotting folder into your workspace directory.

In Eclipse, choose New → Java Project. Give it the exact name trainspotting to use the existing sources.

When running or debugging the Main class in Eclipse, you will need to specify the command-line arguments via the Run Configurations dialog.

Building tsim on your own machine

If you wish to work on this assignment on your own computer, then you will also need
to build and run tsim locally.
We do not recommend this, since it may not be straightforward,
especially if you are not using Linux.

First download the tsim simulator source code.
Then build with the following commands:

$ ./configure
$ make
$ make install

The default installation path is /usr/local. If you want to install to some
other location instead run ./configure --prefix=/your/location. For more
information, see the INSTALL file.

Tips and Tricks

Which parts of track are critical resources?

There are two common ways of writing the train programs. One is that the train
remembers in which place on the track it is and performs its actions based on
that (stateful solution). The other is that the train decides what to do based
on the coordinates of the sensor it steps on (stateless solution). In the
stateless solution, you might still have to remember some things, for example
which semaphors you are holding.

Do not try to solve the general problem. Create a solution that works for this
particular exercise. For example, it is OK to assume that there will be only 2
trains on the map. If you are interested in solving the general problem, write
a short description of how to solve it in the documentation file.

If you have to lock two critical sections at the same time, this probably
indicates that your train flow will be poor. Perhaps you need a little bit
more sensors?

It is not necessary for trains to stop exactly at the end of the track at a station (before changing direction).

Do not overoptimise modularity. The assignment is simple enough to have the
solution in one file.

For testing, run multiple instances of the simulator (at a high simulation
speed), with different train speeds for a long time. e.g. Test your trains with
the first going very fast and the second going very slow, and vice
versa. Don't set the simulation speed too high (--speed switch should be
about 15 or higher), since the trains might not get commands from your program
on time.

Note that get_sensor(TId)blocks until the given train (TId) next
passes a sensor.

In previous courses, the best solutions had usually less than 300 lines of
code. If your solution is much bigger than that then you are probably making
it too complicated. Try making it simpler. It is very important that your code
is understandable.

There is no need to have more than a single try-catch in the entire
program (more
information).

Note!

It was stated above but may be stated again: Reasonable code quality is
expected. You are programmers – programming should be an art to you. The quality
of your code will be vital for its reuse and maintenance.

Common reasons for rejection

Polling/busy waiting

What is polling and what is not is a hard question. We here give some examples
in pseudo code. Loops that reduce to something similar to the situation below
(where the dots do not include any blocking wait) must be considered as
polling.

while (e) { // POLLING!
...
sleep(t);
...
}

Using a blocking operation within a loop is not considered as polling.

while (e) { // NO POLLING!
...
wait(o);
...
}

If it is not the case that something is regularly waking the process from its
wait. Thus, the following example must be considered as an instance of polling: