Archive

Android

STEM Speed Ramp

The STEM Speed Ramp

Styling the app

Android apps are styled using stylesheets in a similar way to the cascading stylesheets used in web pages. In order to figure this step of the process out I referenced the android documentation at http://developer.android.com/guide/topics/ui/themes.html.

The first thing I did was to try the process out so I created a new stylesheet in my app resources values folder called rampstyles.xml. I used the Android XML Values file option from the new file dialog just in case there was something special required (there isn’t as far as I can tell). I then added a test base style to apply to the controls just to make sure the process I was following did what I thought it should do.

At this stage the content of the rampstyles.xml file looked as follows.

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
    <style name="rampcontrol">
        <item name="android:background">#ff0000ff</item>
    </style>    
</resources>

I applied the style through the dialog editor in Eclipse by selecting the bottom level linearLayout and setting the style to the new style name.

Setting the background test style

Setting the background test style

As expected the form turned blue so I went ahead and carried on with the style setting.

For this project I was provided with a splash screen by an external design agency so I decided that in order to give the app a consistent look and feel, I’d use a palette of colours for the forms that came from the image provided to me. Since I needed the image for the splash screen anyway, I started by importing it into the project.

App splash screen

App splash screen

First I created a drawable folder under the folder res and then I imported the splash screen image into it. I was then able to open the image and sample colours from it, turn them into hex values and use them in the styles.

I started by doing some easy stuff. I defined a background colour for the activity and another for the tab pages. These were defined as follows in the rampstyles.xml file.

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
    <style name="background">
        <item name="android:background">#FF136688</item>        
    </style>
    <style name="tabpage">
        <item name="android:background">#ff1a9ad6</item>
    </style>    
</resources>

They were then added to the layout to set the relevant colours.

Having done that I then decided to tackle the somewhat more complicated TabWidget. I searched all over the place to find a clear, concise way of doing it, eventually settling on the a great code snippets blog article here: http://maxalley.wordpress.com/2012/10/27/android-styling-the-tabs-in-a-tabwidget/.

The first thing I did was to add the additional layout and drawable xml files. I started off by using Max’s ones as they appear in his blog. Once that was done, I added a version of his getTabIndicator function to my MainActivity class and updated the setIndicator calls to use it. The resulting updates to the buildTabs method and the new getTabIndicator method are below.

/**
 * Set up the tab control with the correct pages.
 */
private void buildTabs() {
    // get a reference to the tab host
    TabHost tabHost = (TabHost)findViewById(R.id.tabhost);
    
    // set it up
    tabHost.setup();
    
    // create the easy tab
    TabSpec spec1 = tabHost.newTabSpec("Easy");
    spec1.setContent(R.id.easyTab);
    spec1.setIndicator(this.getTabIndicator(tabHost.getContext(), R.string.tab_caption_easy));

    // create the hard tab
    TabSpec spec2 = tabHost.newTabSpec("Hard");
    spec2.setContent(R.id.hardTab);
    spec2.setIndicator(this.getTabIndicator(tabHost.getContext(), R.string.tab_caption_hard));
    
    // create the settings tab
    TabSpec spec3 = tabHost.newTabSpec("Settings");
    spec3.setContent(R.id.settingsTab);
    spec3.setIndicator(this.getTabIndicator(tabHost.getContext(), R.string.tab_caption_settings));
        
    // add them to the tab host
    tabHost.addTab(spec1);
    tabHost.addTab(spec2);
    tabHost.addTab(spec3);
}

/**
 * Inflates the tab indicator, sets the caption and returns the new view.
 * @param context
 * @param title
 * @return
 */
private View getTabIndicator(Context context, int title) {
    View view = LayoutInflater.from(context).inflate(R.layout.tab_layout, null);
    TextView tv = (TextView) view.findViewById(R.id.textView);
    tv.setText(title);
    return view;
}

Running up the app on the device now shows the new styles being applied. The next stage was then to get the styles to display with the colours that I wanted. In order not to have to keep entering the colour values and to make it easier to change things if required, I created a color.xml file in my values folder and added in the colours I picked from the splash screen image.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="background">#FF136688</color>      
    <color name="tabpagebackground">#FF1A9AD6</color>
    <color name="highlight">#FF58B5E0</color>    
    <color name="labeltext">#FFFFFFFF</color>    
</resources>

I then updated my rampstyles.xml file to use the new colours.

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
    <style name="background">
        <item name="android:background">@color/background</item>        
    </style>
    <style name="tabpage">
        <item name="android:background">@color/tabpagebackground</item>
    </style>    
</resources>

Running that up on the device showed that everything was still working so I went on to update some of the tab settings. For all but the unselected file, I removed the second item and updated the colour values to point at colours in color.xml. I left the second item in the unselected because I set the background colour to the same value as the root linearLayout and I wanted them to be framed. I also set the colour of the text in tab_layout.xml to white (@color/labeltext) so it’s the same as the rest of the text. Finally I updated tab_pressed.xml to use @color/highlight so it stands out a bit when the user presses on a tab.

The final bit of styling was to do something with the various buttons. In the specification, they were green and a lot smaller. I wasn’t keen to make them green because it looks horrible so I found another colour scheme, again based on the image provided for the splash screen. I then used an online button generator to create something a little more interesting and added the resulting images into my resources along with a selector (button_selector.xml) for the different images.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:drawable="@drawable/button_pressed" android:state_pressed="true" />
    <item android:drawable="@drawable/button_focused" android:state_focused="true" />
    <item android:drawable="@drawable/button_default" />
</selector>

Updating activity_main to get the buttons in there was a bit of a trauma but eventually I found a combination of layout types and settings that centred by button where I needed it. To get this to happen I had to convert the linear layouts for each tab page to vertical ones then after the TableLayout add a new RelativeLayout in each one to contain the appropriate button. The button settings then include android:layout_centerInParent=”true” to get the button to display in the centre. If you’d like to see the detail of that feel free to grab the code from GitHub and have a look.

The only other style change I made was to add a bit of padding to the labels on the left. I did this by transferring as many of the label settings as I could to a new style called tablabel and then added a padding item to the end. The style in rampstyles.xml ended up looking like this.

	<style name="tablabel">
	    <item name="android:layout_width">wrap_content</item>
	    <item name="android:layout_height">wrap_content</item>
	    <item name="android:height">@dimen/max_setting_label_height</item>
	    <item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
	    <item name="android:width">@dimen/max_setting_label_width</item> 
	    <item name="android:paddingLeft">5dp</item>	    
	</style>  

Adding the Splash Screen

Now I’m not a huge fan of pointless splash screens. If they’re keeping the user occupied while something fairly time consuming is going on then fine but in the case of this app, a splash screen was requested regardless. The customer is, however, always right so a splash screen it is.

As I mentioned perviously, an external design agency was commissioned to create an image for the screen and this was imported into the project in a previous step when I grabbed colours out of it to use as the palette for the data entry screens.

I started by creating a new activity called SplashActivity and setting it as the launcher activity. In the Android manifest I removed the intent filter for MainActivity so there was only one. I replaced the default text view with an image view and created a style for it. The activity_splash.xml finished up with the following.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SplashActivity" >

    <ImageView
        android:id="@+id/imageView1"
        android:contentDescription="@string/splash_image_content_description"
        style="@style/splashimage" />

</RelativeLayout>

The style splashimage is set in rapstyles.xml as below.

<style name="splashimage">
    <item name="android:src">@drawable/siemenssplashscreen</item>
    <item name="android:layout_width">wrap_content</item>
    <item name="android:layout_height">wrap_content</item>
    <item name="android:layout_alignParentLeft">true</item>
    <item name="android:layout_alignParentTop">true</item>
    <item name="android:scaleType">fitXY</item>
</style>

The SplashActivity code is also pretty straightforward.

package com.siemens.stem;

import android.os.Bundle;
import android.os.Handler;
import android.view.Window;
import android.view.WindowManager;
import android.app.Activity;
import android.content.Intent;

/**
 * Implements the splash activity.
 * @author Jon Masters
 *
 */
public class SplashActivity extends Activity {

    /*
     * The time the splash screen stays active in ms
     */
    static private int SPLASH_TIME = 3000; 
    
    /**
     * Called when the activity is first created.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // put the splash screen into full screen.
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 
                WindowManager.LayoutParams.FLAG_FULLSCREEN);
            
        setContentView(R.layout.activity_splash);
    }

    /**
     * Called once the activity is displayed
     */
    protected void onResume() {
        super.onResume();
        
        // create a new runnable to handle an event to move to
        // the main activity.
        new Handler().postDelayed(new Runnable()
        {
            @Override
            public void run()
            {
                //Finish the splash activity so it can't be returned to.
                SplashActivity.this.finish();
                
                // Create an Intent that will start the main activity.
                Intent mainIntent = new Intent(SplashActivity.this, MainActivity.class);
                SplashActivity.this.startActivity(mainIntent);
            }
        }, SPLASH_TIME);
    }
}

Finishing touches

The app specification calls for the screens to be locked in portrait mode. This is done through the activity configuration xml. I applied the change to both activities because the splash screen would look pretty odd in landscape too.

android:screenOrientation="sensorPortait"

I had to target Android 2.3.3 which is API level 10. In order for the activities to lock into portrait mode either way up depending on the sensor, you have to go with the Google typo (thanks StackOverflow!)

The last thing I did was to create a new icon for the app. I did this by finding an area of the splash screen that looked a bit interesting and then cutting out a 96×96 pixel section. I then scaled down the cut out piece to the other resolutions required i.e. 72×72, 48×48 and 32×32 and imported them into the appropriate drawable folder. I then deleted the old ones and renamed the new ones to replace them.

The final app screens

The final app screens

At this point I decided to call it a day. As is usual in these things, there are a raft of changes I’d like to make to improve the code and the user experience. For one thing, other than the app icon, I’ve not even thought about different resolutions and devices. This might change when I get the updated hardware at which point perhaps I’ll pop up an ‘epilogue’ post. In the meantime though, I’ve uploaded the code so you are welcome to browse through and pull out anything of interest. If you have suggestions for improvements or any (constructive!) feedback I’d be really happy to receive it.

I’ve published all of the source code for this project on GitHub if you’d like to look at it / borrow it. The repo can be found here: https://github.com/jpmasters/stem-ramp-configurator

If you have any questions, comments or suggestions please feel free to let me know.

STEM Speed Ramp

The STEM Speed Ramp

Storing the settings

Of all the data captured by the app, the only data I want to persist is the settings data. The rest of it must be entered each time by the students. I decided to use ‘internal storage’ to do this because the data is private to the application and using a database for it seems like overkill.

I decided to persist the data using a simple serialisable settings object. To get things going I just created a new class by right clicking my code namespace in the Package Explorer and created a new class called SettingsData. I added java.io.Serializable to the list of implemented interfaces and clicked Finish. I added the default serialVersionUID by hovering over the orange warning marker and selecting it from the presented options.

I then added private member variables for each of the various fields required, hovered over each of them with my mouse and used the ‘quick fixes’ box to add getters and setters (or accessors and mutators if you’re posh).

The final class is defined as follows.

package com.siemens.stem;

import java.io.Serializable;

/**
 * The serialisable settings for the ramp configurator.
 * @author Jon Masters
 *
 */
public final class SettingsData implements Serializable {

    /**
     * Version information for the class.
     */
    private static final long serialVersionUID = 1L;

    /**
     * The host name or IP address of ramp.
     */
    private String host;
    
    /**
     * The port for the ramp.
     */
    private int port;
    
    /**
     * The time to display the speed after each run.
     */
    private float speedDisplayOnTime;
    
    /**
     * The pause time between simulations.
     */
    private float simulationPauseTime;
    
    /**
     * The sped limit for the ramp.
     */
    private int speedLimitThreshold;

    /**
     * Gets the host name / ip
     * @return the host / ip
     */
    public String getHost() {
        return host;
    }

    /**
     * sets the host name / ip
     * @param host
     */
    public void setHost(String host) {
        this.host = host;
    }

    /**
     * Get the ramp port.
     * @return the tcp port for the ramp
     */
    public int getPort() {
        return port;
    }

    /**
     * Sets the ramp tcp port.
     * @param port
     */
    public void setPort(int port) {
        this.port = port;
    }

    /**
     * Gets the speed display on time.
     * @return the time the speed is displayed for.
     */
    public float getSpeedDisplayOnTime() {
        return speedDisplayOnTime;
    }

    /**
     * Sets the speed display on time.
     * @param speedDisplayOnTime
     */
    public void setSpeedDisplayOnTime(float speedDisplayOnTime) {
        this.speedDisplayOnTime = speedDisplayOnTime;
    }

    /**
     * Gets the simulation pause time.
     * @return the simulation pause time.
     */
    public float getSimulationPauseTime() {
        return simulationPauseTime;
    }

    /**
     * sets the simulation pause time.
     * @param simulationPauseTime
     */
    public void setSimulationPauseTime(float simulationPauseTime) {
        this.simulationPauseTime = simulationPauseTime;
    }

    /**
     * Gets the speed limit for the ramp.
     * @return the speed limit.
     */
    public int getSpeedLimitThreshold() {
        return speedLimitThreshold;
    }

    /**
     * sets the ramp speed limit.
     * @param speedLimitThreshold
     */
    public void setSpeedLimitThreshold(int speedLimitThreshold) {
        this.speedLimitThreshold = speedLimitThreshold;
    }
}

Once that was done I wrote some additional methods to populate the controls from the settings object, the settings object from the controls and load and save the settings object to a file private to the app. I also added a member variable to the MainActivity class and refactored it a little to make the onCreate method a little easier to read. I then updated the onCreate method to load the settings from the private file and also to register an event handler for the ‘send’ button in the settings tab that saves the settings to the private file. The resulting MainActivity class was as follows.

package com.siemens.stem;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StreamCorruptedException;
import java.util.Arrays;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.View;
import android.widget.Button;
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;
import android.widget.EditText;

/**
 * Defines the class that implements the tab controls that are used
 * to enter the data for the ramp.
 * @author Jon Masters
 * 
 */
public class MainActivity extends Activity {

    /**
     * defines the filename for storing the settings.
     */
    static private String SETTINGS_FILE = "settings";
    
    /**
     * Holds the settings for the app.
     */
    private SettingsData settings;
    
    /**
     * Called by the framework to build the options menu.
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    /**
     * Called by the framework when the activity enters the created state. This is where
     * all the initialization needs to happen.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // set up the tab control
        buildTabs();
        
        // load the data for the settings if it is available
        loadSettingsData(); 
        
        // add the settings data to the settings tab
        updateSettingsControls();
        
        // set up the button handlers
        Button settingsSendButton = (Button)findViewById(R.id.buttonSettingsSend);
        settingsSendButton.setOnClickListener(new View.OnClickListener() {
            
            @Override
            public void onClick(View v) {
                
                // save the settings
                MainActivity.this.readSettingsControls();
                MainActivity.this.saveSettingsData();
            }
        });
    }
    
    /**
     * Updates the controls in the activity tabs to reflect the settings data.
     */
    private void updateSettingsControls() {
        
        // we want to leave the controls empty if the settings are at the 
        // default values
        if (this.settings.getHost() != null) {
            EditText host = (EditText)findViewById(R.id.editTextSettingsHost);
            host.setText(this.settings.getHost());
        }
    
        if (this.settings.getPort() != 0) {
            EditText port = (EditText)findViewById(R.id.editTextSettingsPort);
            port.setText(String.valueOf(this.settings.getPort()));
        }
        
        if (this.settings.getSpeedDisplayOnTime() != 0.0f) {
            EditText sdot = (EditText)findViewById(R.id.editTextSpeedDisplayOnTime);
            sdot.setText(String.valueOf(this.settings.getSpeedDisplayOnTime()));
        }
        
        if (this.settings.getSimulationPauseTime() != 0.0f) {
            EditText spt = (EditText)findViewById(R.id.editTextSimulationPauseTime);
            spt.setText(String.valueOf(this.settings.getSimulationPauseTime()));
        }
    
        if (this.settings.getSpeedLimitThreshold() != 0.0f) {
            EditText slt = (EditText)findViewById(R.id.editTextSpeedLimitThreshold);
            slt.setText(String.valueOf(this.settings.getSpeedLimitThreshold()));
        }
    }

    /**
     * Reads the settings from the controls into the settings object.
     */
    private void readSettingsControls() {
        
        EditText host = (EditText)findViewById(R.id.editTextSettingsHost);
        String hostText = host.getText() == null || host.getText().length() == 0 ?
                null : host.getText().toString();
        this.settings.setHost(hostText);
    
        EditText port = (EditText)findViewById(R.id.editTextSettingsPort);
        String portText = port.getText() == null || port.getText().length() == 0 ?
                "0" : port.getText().toString();
        this.settings.setPort(Integer.parseInt(portText));
        
        EditText sdot = (EditText)findViewById(R.id.editTextSpeedDisplayOnTime);
        String sdotText = sdot.getText() == null || sdot.getText().length() == 0 ?
                "0.0" : sdot.getText().toString();
        this.settings.setSpeedDisplayOnTime(Float.parseFloat(sdotText));
        
        EditText spt = (EditText)findViewById(R.id.editTextSimulationPauseTime);
        String sptText = spt.getText() == null || spt.getText().length() == 0 ?
                "0.0" : spt.getText().toString();
        this.settings.setSimulationPauseTime(Float.parseFloat(sptText));

        EditText slt = (EditText)findViewById(R.id.editTextSpeedLimitThreshold);
        String sltText = slt.getText() == null || slt.getText().length() == 0 ?
                "0.0" : slt.getText().toString();
        this.settings.setSpeedLimitThreshold(Integer.parseInt(sltText));
    }

    /**
     * Load the settings from a private file.
     */
    private void loadSettingsData() {
        
        // check to see if the settings exist
        if (Arrays.asList(fileList()).contains(SETTINGS_FILE)) {

            try {

                // yes it does, try to load it
                ObjectInputStream ois = new ObjectInputStream(
                        openFileInput(SETTINGS_FILE));

                this.settings = (SettingsData)ois.readObject();
                
            } catch (StreamCorruptedException e) {
                e.printStackTrace();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        
        // if there were any problems or the file didn't exist
        // use an new empty object
        if (this.settings == null) {
            // create an empty one to hold the data
            this.settings = new SettingsData();
        }
    }

    /**
     * Save the settings data to a private file.
     */
    private void saveSettingsData() {
    
        try {
            
            ObjectOutputStream oos = new ObjectOutputStream(openFileOutput(SETTINGS_FILE, 0));
            oos.writeObject(this.settings);
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }       
    }

    /**
     * Set up the tab control with the correct pages.
     */
    private void buildTabs() {
        // get a reference to the tab host
        TabHost tabHost = (TabHost)findViewById(R.id.tabhost);
        
        // set it up
        tabHost.setup();
        
        // create the easy tab
        TabSpec spec1 = tabHost.newTabSpec("Easy");
        spec1.setContent(R.id.easyTab);
        spec1.setIndicator(getString(R.string.tab_caption_easy));

        // create the hard tab
        TabSpec spec2 = tabHost.newTabSpec("Hard");
        spec2.setContent(R.id.hardTab);
        spec2.setIndicator(getString(R.string.tab_caption_hard));
        
        // create the settings tab
        TabSpec spec3 = tabHost.newTabSpec("Settings");
        spec3.setContent(R.id.settingsTab);
        spec3.setIndicator(getString(R.string.tab_caption_settings));
        
        // add them to the tab host
        tabHost.addTab(spec1);
        tabHost.addTab(spec2);
        tabHost.addTab(spec3);
    }
}

Having done all that I ran up the app to test it and everything behaved as expected. It starts up for the first time with no saved settings file and so the code creates a default, empty settings object and loads the controls appropriately i.e. with no values in the edit boxes.

If the user hits the send button, the values from the edit boxes are transferred to the settings object and this is persisted as a private file to the app. When the app is next reloaded, the previous settings are transferred to the settings controls in the form.

Transmitting Data to the ramp

For the next step, I chose to implement the core of the code that sends the data to the ramp. As I didn’t have the ramp handy I started by implementing a very simple ramp simulator. I did this by creating a new Java application and adding a class with a main() method. The class simply creates a socket listener on a port passed in on the command line and when a TCP connection is made, responds to text commands by repeating the received line and sending back the ramp prompt as defined by the interface specification.

The complete listing of the simulator is as follows.

package com.siemens.rampsim;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class Application {

    static String APP_OUT = "\nRamp Simulator\n==============\n";
    static String APP_PARAMS_OUT = "Listening on port %d\n\n";
    static String USAGE_TEXT = "USAGE:\njava com.siemens.rampsim.Application [PORT]";
    static String CONN_ACCEPTED = "Connection accepted from client %s";
    static String CLIENT_DISCONNECT = "\n\nClient %s has disconnected.";
    static String RAMP_PROMPT = "ramp>";
    
    /**
     * @param args
     */
    public static void main(String[] args) {

        System.out.println(APP_OUT);
        
        if (args.length != 1) {
            System.out.println(USAGE_TEXT);
            return;
        }
    
        int port = Integer.parseInt(args[0]);
        
        try {
            
            ServerSocket rampSocket = new ServerSocket(port);
            
            while(true) {
                
                System.out.println(String.format(APP_PARAMS_OUT, port));
                Socket sock = rampSocket.accept();
                                
                InetSocketAddress addr = (InetSocketAddress)sock.getRemoteSocketAddress();
                String remoteHost = addr.getHostName();
                
                System.out.println(String.format(CONN_ACCEPTED, remoteHost));
                BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
                DataOutputStream dos = new DataOutputStream(sock.getOutputStream());
                
                while(true) {
                    
                    String receivedData = br.readLine();

                    // check that we still have a connection
                    if (receivedData != null) {
                        dos.writeBytes(receivedData);
                        dos.writeBytes("\n");
                        System.out.println(receivedData);
                        
                        dos.writeBytes(RAMP_PROMPT);
                        System.out.print(RAMP_PROMPT);
                    }
                    else {
                        break;
                    }
                }
                
                System.out.println(String.format(CLIENT_DISCONNECT, remoteHost));
            }
            
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

The code above provides a very simple way of testing the app and is in no way supposed to represent best practice for implementing a socket server but it does the job I needed it to do. Running up the simulator is simply a case of opening a terminal window, navigating to the application’s bin directory and executing the following command.

java com.siemens.rampsim.Application 10101

The argument 10101 tells the application to listen on that particular port.

The next thing I did was to write a new class to handle the actual transmission of the data.

package com.siemens.stem;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;

/**
 * This is a static class that is used to write the configuration data to the ramp.
 * @author Jon Masters
 *
 */
class RampWriter {

    static String HEADWAY = "HEADWAY=%d\n";
    static String DISTCONV = "DISTCONV=%d\n";
    static String TIMECONV = "TIMECONV=%d\n";
    static String CARSCALE = "CARSCALE=%d\n";
    static String GRAVITY = "GRAVITY=%s\n";
    static String DROPTIME = "DROPTIME=%d\n";
    static String CARACCEL = "CARACCEL=%s\n";
    static String SPEEDDISPLAY = "SPEEDDISPLAY=%s\n";
    static String SPEEDLIMITTHRESH = "SPEEDLIMITTHRESH=%d\n";
    static String SIMPAUSE = "SIMPAUSE=%s\n";
    
    protected RampWriter(){}
    
    /**
     * Writes the settings from the easy tab to the ramp
     * @param host - the ramp ip address or host name
     * @param port - the port the ramp is listening on
     * @param distanceBetweenLoops - the distance between the loops used for speed calculation
     * @param distanceConversionFactor - the distance conversion factor
     * @param timeConversionFactor - the time conversion factor
     * @param carScale - the scale of the model car
     * @param ballDropTime - the time it takes for the ball to drop
     * @throws IOException
     * @throws IllegalArgumentException
     */
    static void writeEasySettings(InetAddress host, int port, int distanceBetweenLoops, 
            int distanceConversionFactor, int timeConversionFactor, int carScale, int ballDropTime) 
            throws IOException, IllegalArgumentException {
        
        Socket client = null;
        PrintWriter out = null;
        BufferedReader in = null;
        
        try {
            
            // open a connection to the ramp
            client = new Socket(host, port);
            out = new PrintWriter(client.getOutputStream());
            in = new BufferedReader(new InputStreamReader(client.getInputStream()));
            
            // start by writing a line feed which should result in the test 'ramp>' back
            // from the ramp
            out.write("\n");    
            out.flush();
            readFromRamp(in);
            
            // write the settings out to the ramp
            out.write(String.format(HEADWAY, distanceBetweenLoops));
            out.flush();
            readFromRamp(in);
            
            out.write(String.format(DISTCONV, distanceConversionFactor));
            out.flush();
            readFromRamp(in);
            
            out.write(String.format(TIMECONV, timeConversionFactor));
            out.flush();
            readFromRamp(in);
            
            out.write(String.format(CARSCALE, carScale));
            out.flush();
            readFromRamp(in);
            
            out.write(String.format(DROPTIME, ballDropTime));
            out.flush();
            readFromRamp(in);        
        }
        finally {
            // make sure we close anything that needs to be closed
            if (in != null) {
                in.close();
            }
            
            if (out != null) {
                out.close();
            }
            
            if (client != null && client.isConnected()) {
                client.close();
            }
        }
    }

    /**
     * Writes the settings for the hard tab to the ramp.
     * @param host - the ramp IP or host name
     * @param port - the port the ramp is listening on
     * @param distanceBetweenLoops - the distance between the loops
     * @param carScale - the scale of the vehicle
     * @param accelerationOfGravity - the gravitational constant
     * @param ballDropTime - the time taken for the ball to drop
     * @param carAcceleration - the acceleration of the car
     * @throws IOException
     * @throws IllegalArgumentException
     */
    static void writeHardSettings(InetAddress host, int port, int distanceBetweenLoops, 
            int carScale, float accelerationOfGravity, int ballDropTime, float carAcceleration) 
            throws IOException, IllegalArgumentException {
        
        Socket client = null;
        PrintWriter out = null;
        BufferedReader in = null;
        
        try {
            
            // open a connection to the ramp
            client = new Socket(host, port);
            out = new PrintWriter(client.getOutputStream());
            in = new BufferedReader(new InputStreamReader(client.getInputStream()));
            
            // start by writing a line feed which should result in the test 'ramp>' back
            // from the ramp
            out.write("\n");    
            out.flush();
            readFromRamp(in);
            
            // write the settings out to the ramp
            out.write(String.format(HEADWAY, distanceBetweenLoops));
            out.flush();
            readFromRamp(in);
            
            out.write(String.format(CARSCALE, carScale));
            out.flush();
            readFromRamp(in);
            
            out.write(String.format(GRAVITY, accelerationOfGravity));
            out.flush();
            readFromRamp(in);
                        
            out.write(String.format(DROPTIME, ballDropTime));
            out.flush();
            readFromRamp(in);        
            
            out.write(String.format(CARACCEL, carAcceleration));
            out.flush();
            readFromRamp(in);

        }
        finally {
            // make sure we close anything that needs to be closed
            if (in != null) {
                in.close();
            }
            
            if (out != null) {
                out.close();
            }
            
            if (client != null && client.isConnected()) {
                client.close();
            }
        }
    }
    
    /**
     * Writes the settings for the ramp to the ramp.
     * @param host - the ramp IP or host name
     * @param port - the port the ramp is listening on
     * @param speedDisplayTime - the length of time the speed should be displayed
     * @param simulationPauseTime - the gap between simulated runs in sim mode.
     * @param speedLimitThreshold - the speed limit for the ramp
     * @throws IOException
     * @throws IllegalArgumentException
     */
    static void writeRampSettings(InetAddress host, int port, float speedDisplayTime, 
            float simulationPauseTime, int speedLimitThreshold) 
            throws IOException, IllegalArgumentException {
        
        Socket client = null;
        PrintWriter out = null;
        BufferedReader in = null;
        
        try {
            
            // open a connection to the ramp
            client = new Socket(host, port);
            out = new PrintWriter(client.getOutputStream());
            in = new BufferedReader(new InputStreamReader(client.getInputStream()));
            
            // start by writing a line feed which should result in the test 'ramp>' back
            // from the ramp
            out.write("\n");    
            out.flush();
            readFromRamp(in);
            
            // write the settings out to the ramp
            out.write(String.format(SPEEDDISPLAY, speedDisplayTime));
            out.flush();
            readFromRamp(in);
            
            out.write(String.format(SPEEDLIMITTHRESH, speedLimitThreshold));
            out.flush();
            readFromRamp(in);
            
            out.write(String.format(SIMPAUSE, simulationPauseTime));
            out.flush();
            readFromRamp(in);                       
        }
        finally {
            // make sure we close anything that needs to be closed
            if (in != null) {
                in.close();
            }
            
            if (out != null) {
                out.close();
            }
            
            if (client != null && client.isConnected()) {
                client.close();
            }
        }
    }   
    
    /**
     * Reads the response to a command from the ramp.
     * @param in - the buffered read around the socket connection inpu stream
     * @throws IOException
     * @throws IllegalArgumentException
     */
    private static void readFromRamp(BufferedReader in) throws IOException,
            IllegalArgumentException {
        
        // keep reading from the socket until we get the prompt
        StringBuffer sb = new StringBuffer();
        int nextChar = 0;
        while(nextChar != -1) {
            nextChar = in.read();
            char c = (char)nextChar;
            sb.append(c);
            if (sb.toString().endsWith("ramp>")) {
                break;
            }
        }
        
        // if we didn't end with ramp> then we're not talking to a ramp
        if (!sb.toString().endsWith("ramp>")) {
            throw new IllegalArgumentException(
                "The ramp did not respond as expected. Check that the host and port settings are correct.");
        }
    }
}

I then updated the setting button handler to send the data to the new class.

settingsSendButton.setOnClickListener(new View.OnClickListener() {
			
	@Override
	public void onClick(View v) {
				
		// save the settings
		MainActivity.this.readSettingsControls();
		MainActivity.this.saveSettingsData();
				
		// get a reference to the settings
		SettingsData rampSettings = MainActivity.this.settings;
				
		// send the config data to the ramp
		try {
			
			RampWriter.writeRampSettings(
				InetAddress.getByName(rampSettings.getHost()), 
				rampSettings.getPort(), 
				rampSettings.getSpeedDisplayOnTime(), 
				rampSettings.getSimulationPauseTime(), 
				rampSettings.getSpeedLimitThreshold());
					
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
});

Having done this, I then added another data class, this one not persisted, that holds data retrieved from the controls when the ‘send’ buttons are pressed on either the ‘easy’ or ‘hard’ tabs. Rather than create a different class for each I created a single class to capture this data. The main reason for doing it this way was that several of the settings are used by both tabs and it just seemed wrong to be holding that data twice. I therefore added a new class called WorksheetData and added in the required fields, getters and setters (it’s at times like these that you really miss languages that allow you to specify members, getters and setters in a single line!)

package com.siemens.stem;

/**
 * This is a model class that holds the worksheet data from the
 * easy and hard tabs.
 * @author Jon Masters
 *
 */
class WorksheetData {

    private int distanceBetweenLoops; 
    
    private int distanceConversionFactor;
    
    private int timeConversionFactor; 
    
    private int carScale;
    
    private int ballDropTime;
    
    private float accelerationOfGravity;
    
    private float carAcceleration;
    
    public int getDistanceBetweenLoops() {
        return distanceBetweenLoops;
    }
    
    public void setDistanceBetweenLoops(int distanceBetweenLoops) {
        this.distanceBetweenLoops = distanceBetweenLoops;
    }

    public int getDistanceConversionFactor() {
        return distanceConversionFactor;
    }

    public void setDistanceConversionFactor(int distanceConversionFactor) {
        this.distanceConversionFactor = distanceConversionFactor;
    }

    public int getTimeConversionFactor() {
        return timeConversionFactor;
    }

    public void setTimeConversionFactor(int timeConversionFactor) {
        this.timeConversionFactor = timeConversionFactor;
    }

    public int getCarScale() {
        return carScale;
    }

    public void setCarScale(int carScale) {
        this.carScale = carScale;
    }

    public int getBallDropTime() {
        return ballDropTime;
    }

    public void setBallDropTime(int ballDropTime) {
        this.ballDropTime = ballDropTime;
    }

    public float getAccelerationOfGravity() {
        return accelerationOfGravity;
    }

    public void setAccelerationOfGravity(float accelerationOfGravity) {
        this.accelerationOfGravity = accelerationOfGravity;
    }

    public float getCarAcceleration() {
        return carAcceleration;
    }

    public void setCarAcceleration(float carAcceleration) {
        this.carAcceleration = carAcceleration;
    }
}

There’s absolutely nothing special about the WorksheetData class, it really is just a holder for data. The next thing I did was to add a WorksheetData member to the MainActivity class and also new methods to populate the WorksheetData class from the various controls. I implemented an ‘Easy’ tab version and a ‘Hard’ tab version.

Inside the onCreate method I added the following additional code to handle the button clicks on the easy and hard tabs.

Button easySendButton = (Button)findViewById(R.id.buttonEasySend);
easySendButton.setOnClickListener(new View.OnClickListener() {
    
    @Override
    public void onClick(View v) {
        
        // read the data from the controls into the data object
        MainActivity.this.readEasyWorksheetDataControls();
        
        // get an easy ref to the data
        WorksheetData wsd = MainActivity.this.worksheetData;
        SettingsData sd = MainActivity.this.settings;
        
        try {
            
            RampWriter.writeEasySettings(
                    InetAddress.getByName(sd.getHost()), 
                    sd.getPort(), 
                    wsd.getDistanceBetweenLoops(), 
                    wsd.getDistanceConversionFactor(), 
                    wsd.getTimeConversionFactor(), 
                    wsd.getCarScale(), 
                    wsd.getBallDropTime());
            
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
});

Button hardSendButton = (Button)findViewById(R.id.buttonHardSend);
hardSendButton.setOnClickListener(new View.OnClickListener() {
    
    @Override
    public void onClick(View v) {
        
        // read the data from the controls into the data object
        MainActivity.this.readHardWorksheetDataControls();
        
        // get an easy ref to the data
        WorksheetData wsd = MainActivity.this.worksheetData;
        SettingsData sd = MainActivity.this.settings;
        
        try {
            
            RampWriter.writeHardSettings(
                    InetAddress.getByName(sd.getHost()), 
                    sd.getPort(), 
                    wsd.getDistanceBetweenLoops(), 
                    wsd.getCarScale(), 
                    wsd.getAccelerationOfGravity(), 
                    wsd.getBallDropTime(), 
                    wsd.getCarAcceleration());
            
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
});

I then added the two missing methods to the MainActivity class.

/**
 * Reads the worksheet data from the controls into the data object.
 */
private void readEasyWorksheetDataControls() {
    
    // make sure the worksheet data member has been created
    if (this.worksheetData == null) {
        this.worksheetData = new WorksheetData();
    }
    
    EditText loopDistCtrl = (EditText)findViewById(R.id.editTextEasyDistanceBetweenLoops);
    String loopDistText = loopDistCtrl.getText() == null || loopDistCtrl.getText().length() == 0 ?
            "0" : loopDistCtrl.getText().toString();
    this.worksheetData.setDistanceBetweenLoops(Integer.parseInt(loopDistText));
    
    EditText distConvCtrl = (EditText)findViewById(R.id.editTextEasyDistanceConversionFactor);
    String distConvText = distConvCtrl.getText() == null || distConvCtrl.getText().length() == 0 ?
            "0" : distConvCtrl.getText().toString();
    this.worksheetData.setDistanceConversionFactor(Integer.parseInt(distConvText));
        
    EditText timeConvCtrl = (EditText)findViewById(R.id.editTextEasyTimeConversion);
    String timeConvText = timeConvCtrl.getText() == null || timeConvCtrl.getText().length() == 0 ?
            "0" : timeConvCtrl.getText().toString();
    this.worksheetData.setTimeConversionFactor(Integer.parseInt(timeConvText));
        
    EditText carScaleCtrl = (EditText)findViewById(R.id.editTextEasyCarScale);
    String carScaleText = carScaleCtrl.getText() == null || carScaleCtrl.getText().length() == 0 ?
            "0" : carScaleCtrl.getText().toString();
    this.worksheetData.setCarScale(Integer.parseInt(carScaleText));

    EditText ballDropCtrl = (EditText)findViewById(R.id.editTextEasyBalldropTime);
    String ballDropText = ballDropCtrl.getText() == null || ballDropCtrl.getText().length() == 0 ?
            "0.0" : ballDropCtrl.getText().toString();
    this.worksheetData.setBallDropTime(Integer.parseInt(ballDropText));
}
    
/**
 * Reads the worksheet data from the controls into the data object.
 */
private void readHardWorksheetDataControls() {
        
    // make sure the worksheet data member has been created
    if (this.worksheetData == null) {
        this.worksheetData = new WorksheetData();
    }
        
    EditText loopDistCtrl = (EditText)findViewById(R.id.editTextHardDistanceBetweenLoops);
    String loopDistText = loopDistCtrl.getText() == null || loopDistCtrl.getText().length() == 0 ?
            "0" : loopDistCtrl.getText().toString();
    this.worksheetData.setDistanceBetweenLoops(Integer.parseInt(loopDistText));

    EditText carScaleCtrl = (EditText)findViewById(R.id.editTextHardCarScale);
    String carScaleText = carScaleCtrl.getText() == null || carScaleCtrl.getText().length() == 0 ?
            "0" : carScaleCtrl.getText().toString();
    this.worksheetData.setCarScale(Integer.parseInt(carScaleText));

    EditText accGravityCtrl = (EditText)findViewById(R.id.editTextHardAccelerationGravity);
    String accGravityText = accGravityCtrl.getText() == null || accGravityCtrl.getText().length() == 0 ?
            "0.0" : accGravityCtrl.getText().toString();
    this.worksheetData.setAccelerationOfGravity(Float.parseFloat(accGravityText));
        
    EditText ballDropCtrl = (EditText)findViewById(R.id.editTextHardBallDropTime);
    String ballDropText = ballDropCtrl.getText() == null || ballDropCtrl.getText().length() == 0 ?
            "0" : ballDropCtrl.getText().toString();
    this.worksheetData.setBallDropTime(Integer.parseInt(ballDropText));

    EditText carAccelCtrl = (EditText)findViewById(R.id.editTextHardCarAcceleration);
    String carAccelText = carAccelCtrl.getText() == null || carAccelCtrl.getText().length() == 0 ?
            "0.0" : carAccelCtrl.getText().toString();
    this.worksheetData.setCarAcceleration(Float.parseFloat(carAccelText));
}

A quick test with the ramp simulator showed that this was working however there were quite a few areas where there could be errors. For example, what happens if the settings haven’t been entered before the student tries to send the worksheet data? What happens if the host or port is entered but entered incorrectly?

A bit of experimentation showed that when the host information has been incorrectly entered showed that it would throw an IOException. When the port is incorrectly entered, an IllegalArgumentException is thrown. The simplest way to get this information to user seemed to be to use the Android Toast mechanism to display an error message so I updated the catch handlers in the button onClick handlers to display a fixed error message defined in the strings.xml and formatted to include the exception message. The code added to each of the catch blocks was as follows.

    MainActivity.this.displayError(
        String.format(MainActivity.this.getString(
            R.string.ramp_connection_error), e.getMessage()));

Testing this with deliberately wrong host or port information results in an error message being displayed as show below.

Connection error display

Connection error display

Now that I had this functionality complete, I decided to get the styling and splash screen in place.

In the next article I’ll discuss how I styled the screens and added the splash screen. In the meantime if you’d like to see the finished code, I’ve uploaded it to GitHub here: https://github.com/jpmasters/stem-ramp-configurator.

STEM Speed Ramp

The STEM Speed Ramp

Understanding the Structure of Android Apps

Before diving straight into the development, it occurred to me that it might be a good idea to have a quick scan through the Android documentation to see how the application lifecycle works and what I need to do to get the thing going. The relevant Android documentation can be found here:

http://developer.android.com/training/basics/activity-lifecycle/index.html

Android apps are built around a set of ‘Activities’ that move through a set of states over course of their lifetime as shown in the diagram below.

The Android Activity lifecycle

The Android Activity lifecycle (source Google)

Of the states described above, the app will spend the majority of time in the Resumed, Paused or Stopped states and it’s the transitions between these that need to be looked at carefully because I need to make sure that the app doesn’t crash if the students re-orientate the screen or switch to another app. If they switch between apps, I also don’t want them to lose any work they’ve already done so while the app is in use, data will need to be persisted. When the app is first started however, we don’t want the data in there still from the previous class so when the app moves to the destroyed state, we want the data to be deleted.

For this app I’m going to need two different activities, one to allow the students to configure the ramp and another to display the splash screen that must be displayed on start-up.

I started off by creating the main ramp configuration screen. Having right-clicked on the namespace in Package Explorer I selected Android and Android Activity and clicked Next. I then selected ‘Blank Activity’ from the options and clicked Next. As I forgot to check the ‘Launcher Activity’ on the way through the screens, I had to manually add the launder information to the activity in AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.siemens.stem"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="10" />

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name="com.siemens.stem.MainActivity"
            android:label="@string/title_activity_main" >
            
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Note that I also changed the minSDKVersion to 10 which is fine for an app running on Android 2.3.3 (see http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels).

Setting up the main screen

I started by opening the file activity_main.xml which opened in a screen editor window. In the portrait view I added in the tab control and the various labels, text fields and buttons. The tab control has three tabs labelled Easy, Hard and Settings. For more advanced students, the ‘Hard’ tab requires them to do more of the configuration work while the ‘Easy’ option is designed for younger students who may not be able to cope with some of the more advanced concepts. The ‘Settings’ tab is where the ramp settings are configured.

Having added the Tab control to the form I then opened activity_main.xml in the XML view and edited some of the values. The resulting activity_main.xml file looked like this:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <TabHost
        android:id="@android:id/tabhost"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true" >
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical" >
    
            <TabWidget
                android:id="@android:id/tabs"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" >
            </TabWidget>
    
            <FrameLayout
                android:id="@android:id/tabcontent"
                android:layout_width="match_parent"
                android:layout_height="match_parent" >
    
                <LinearLayout
                    android:id="@+id/easyTab"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" >
    
                    <TextView
                        android:id="@+id/textView1"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="Easy"
                        android:textAppearance="?android:attr/textAppearanceLarge" />
    
                </LinearLayout>
    
                <LinearLayout
                    android:id="@+id/hardTab"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" >
                    <TextView
                        android:id="@+id/textView1"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="Hard"
                        android:textAppearance="?android:attr/textAppearanceLarge" />
                
                </LinearLayout>
    
                <LinearLayout
                    android:id="@+id/settingsTab"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" >
                    <TextView
                        android:id="@+id/textView1"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="Settings"
                        android:textAppearance="?android:attr/textAppearanceLarge" />
                
                </LinearLayout>
            </FrameLayout>
        </LinearLayout>
    </TabHost>
</RelativeLayout>

While I had the dialog editor open I made a couple of other quick changes too. As I’m targeting a device that supports Android version 2.3.3 I updated the Android version in the dialog editor to 10. I also know that I’m targeting a device that has a 7” screen with a resolution of a measly 480×800 pixels. The closest I could find in the available options in the dialog editor was a 5.1” screen with the 480×800 resolution so I decided to use that for laying out my controls.

Setting up the tab control

I started by giving the TabHost an ID so that we can get at it from the code. In the outline view, I selected the TabHost and opened the ID property setting the name to tabHost.

I then opened the MainActivity class file and added the following code.

package com.siemens.stem;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // get a reference to the tab host
        TabHost tabHost = (TabHost)findViewById(R.id.tabhost);
        
        // set it up
        tabHost.setup();
        
        // create the easy tab
        TabSpec spec1 = tabHost.newTabSpec("Easy");
        spec1.setContent(R.id.easyTab);
        spec1.setIndicator("Easy");

        // create the hard tab
        TabSpec spec2 = tabHost.newTabSpec("Hard");
        spec2.setContent(R.id.hardTab);
        spec2.setIndicator("Hard");
        
        // create the settings tab
        TabSpec spec3 = tabHost.newTabSpec("Settings");
        spec3.setContent(R.id.settingsTab);
        spec3.setIndicator("Settings");
        
        // add them to the tab host
        tabHost.addTab(spec1);
        tabHost.addTab(spec2);
        tabHost.addTab(spec3);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
}

Running that code up on the device allowed me to see the tab control and after I added a label on each of the tab linear layouts just to show which one was visible, I could see it was switching between the tabs just fine.

I was keen though to use resources for the tab text. To do this  I first created the required resource strings in the strings.xml file.

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">STEM Ramp Configurator</string>
    <string name="title_activity_main">Enter Ramp Settings</string>
    <string name="action_settings">Settings</string>
    <string name="tab_caption_easy">Easy</string>
    <string name="tab_caption_hard">Hard</string>
    <string name="tab_caption_settings">Settings</string>

</resources>

While I was in there I also updated some of the other strings that I wanted to customise. Getting the strings out of the resources and using them turns out to be easy as you’d expect. It’s just a case of calling the function getString() and passing in the ID of the string you want to display so I updated the MainActivity class as follows.

package com.siemens.stem;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // get a reference to the tab host
        TabHost tabHost = (TabHost)findViewById(R.id.tabhost);
        
        // set it up
        tabHost.setup();
        
        // create the easy tab
        TabSpec spec1 = tabHost.newTabSpec("Easy");
        spec1.setContent(R.id.easyTab);
        spec1.setIndicator(getString(R.string.tab_caption_easy));

        // create the hard tab
        TabSpec spec2 = tabHost.newTabSpec("Hard");
        spec2.setContent(R.id.hardTab);
        spec2.setIndicator(getString(R.string.tab_caption_hard));
        
        // create the settings tab
        TabSpec spec3 = tabHost.newTabSpec("Settings");
        spec3.setContent(R.id.settingsTab);
        spec3.setIndicator(getString(R.string.tab_caption_settings));
        
        // add them to the tab host
        tabHost.addTab(spec1);
        tabHost.addTab(spec2);
        tabHost.addTab(spec3);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

}

I then did a bit of a test run and all appeared to be well.

The main screen tab controls

The main screen tab controls

Adding the Controls to the Screens

The next step was to add the controls to the first of the tabs. I did this by selecting the ‘easy’ tab’s linear layout, adding a table view and dropping textViews and editTexts into it. I defined the strings for the captions and units in the strings.xml file and set some label width and heights and editText widths in the dimens.xml file which I then set in the relevant property grids.

The updated dimens.xml now looked like this:

<resources>

    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
    <dimen name="max_setting_label_width">200dp</dimen>
    <dimen name="max_setting_label_height">50dp</dimen>
    <dimen name="max_setting_textview_width">150dp</dimen>

</resources>

I also added a ‘Send’ button to the bottom of the grid view and set its caption.

Running up the application and showing the ‘easy’ tab now shows the display as shown below.

Controls on the 'Easy' tab

Controls on the ‘Easy’ tab

Having sorted out the controls on the first tab, I then went on to do the others. I couldn’t figure out a way to display the other tabs and do the layouts until I found the embarrassingly simple answer in a Stack Overflow article. Simply moving the layout you want to edit to the top of the list of tab pages makes it visible then you can rearrange them once you’re done.

Main screen configuration tabs

Main screen configuration tabs

Once all of the controls had been added to the tab pages I ran it up on the device and checked through to make sure I had the correct field lengths and other constraints. I also made sure the edit boxes had recognisable names so I can get at the data they’ll contain easily. I used the best fitting ‘input type’ for the edit boxes. Integer values are restricted to ‘number’ values without decimal points, float values allow the entry of a decimal point and are set to ‘numberDecimal’. The only non-numeric field is the one that allows entry of the ramp host or IP address.

In the next article I’ll discuss how I added the functionality behind the screens. In the meantime if you’d like to see the finished code, I’ve uploaded it to GitHub here: https://github.com/jpmasters/stem-ramp-configurator.

STEM Speed Ramp

The STEM Speed Ramp

Background

As part of the Siemens commitment to supporting local schools, some of the guys and gals in engineering have been working on a project for the national STEM education programme. For those of you who don’t know, STEM stands for Science Technology Engineering and Mathematics and is designed to encourage young people in the UK into these professions (see http://www.nationalstemcentre.org.uk/stem-in-context/what-is-stem).

The project involved creating a ramp that toy cars could run down, their speed detected by a loop detector. This is exactly the same as can be found in real roads and if you look closely at roads and motorways next time you’re in a car you’ll notice where the slots that contain the loops have been cut into the tarmac. By varying the distance up the ramp that the car is released, the speed of the vehicle can be changed as it goes over the detector. The ramp then measures this speed and if the car is travelling below the ‘speed limit’ it lights up a smiley face and if it is travelling above the speed limit, it lights up a red unhappy face and drops a squash ball onto the vehicle (as you do!).

The students have workbooks which lead them through various challenges such as configuring jumpers on the boards and assembling a few of the components. They must also perform some of the calculations required to make the ramp work correctly and then send the results to the ramp.

This is where the Android app comes in. The ramp includes a serial to WiFi module and the firmware which runs on an STM32 board, provides a simple command line interface to support the configuration data entry.

In order to configure the ramp, the Android app simply needs to collect the data, turn it into a set of text commands and send them down a socket to the WiFi module which will convert it into a serial connection and pass it to the STM32 board firmware.

Before starting on the app coding, the ramp firmware engineer and I sat down and decided on these parameters and how they’ll be sent and I was also provided with a document which described in detail how the screens need to be constructed and what configuration data must be captured and sent. The specification included a splash screen, the artwork for which was to be provided by an external design agency.

Configurator screen specifications

Part of the configurator screen specifications

The ramp configuration protocol

The protocol we settled on is completely text based with and end of line marker of either carriage return or line feed. This was done to enable independent testing and setup of the ramp using Telnet should the app not be available for some reason.

When the Android app first makes the connection, it sends a carriage return to the device. All lines sent to the ramp receive a prompt in reply which is simply:

ramp>

Configuration is carried out by sending each parameter one at a time in the following way:

PARAMETER=VALUE

followed by a carriage return or line feed.

The following parameters can be specified:

Parameter Description Min Value Max Value
HEADWAY The distance between the loops in mm 1 9,999
DISTCONV Distance conversion from mm to miles 1 9,999,999
TIMECONV Time conversion seconds to hours 1 99,999
CARSCALE Car scale (1:x) 1 999
GRAVITY Acceleration constant of gravity (m/s2) 0.01 999
DROPTIME Ball drop time to impact (ms) 1 9,999
CARACCEL The expected acceleration of the car (m/s2) 0.01 999
SPEEDDISPLAY The length of time the speed display should be left on in seconds. 0.1 99
SPEEDLIMITTHRESH Speed limit in miles per hour 1 99
SIMPAUSE The pause time between simulated runs in demo mode. 0.1 99

Table 1: STEM Ramp Parameters

A conversation between the app and the ramp to set the headway and distance conversion therefore might therefore go something like this with the app starting things off with a couple of carriage returns to make sure everything is ok:

ramp>
ramp>
ramp>HEADWAY=120
ramp>DISTCONV=1690000
ramp>

Getting the development environment set up

My Android development environment

My Android development environment

In order to get this going, the team provided me with the target Android device. It’s not a particularly great one I have to admit. It has a resistive touch screen and runs Android 2.3.3 so it’s not very sophisticated. I have managed to convince them to look for something a bit better though so there should be a much better device available fairly soon. In the meantime though I’ll work with what we already have.

Having first connected the device to my laptop using the world’s shortest, but unfortunately all that was available, USB cable, I started by opening up Eclipse (I’m using Juno) and creating a new workspace for the project which I called ‘stem ramp’.

New Workspace Dialog

Creating the new workspace

The next thing is to get hold of the Android SDK which you can do from here: http://developer.android.com/sdk/index.html

If you don’t already have Eclipse, it looks like you can download it from the Android site and it’ll have the Android developer tools already packaged in it. Nice. Anyhow, for me I had to do it in the slightly more long-winded way by downloading the zip file containing the tools and extracting it to a local directory. Following the online instructions I then went on to install the Eclipse plug in (http://developer.android.com/sdk/installing/installing-adt.html).

Once that is done, you have to configure the Eclipse tools to point at the Android SDK that was downloaded and extracted earlier. You do this by restarting Eclipse and doing the following:

1. In the “Welcome to Android Development” window that appears, select Use existing SDKs.

2. Browse and select the location of the Android SDK directory you recently downloaded and unpacked.

3. Click Next.

Next I created the Android project through the New-> AWS Android Project menu. I selected a target of Android 2.3.3, named the project ‘STEM Ram Configurator’ and set the namespace to com.siemens.stem.

Having created the project, the next thing I did was to set up the device for debugging using the instructions found here: http://developer.android.com/tools/device.html and gave it a try. I had a bit of trouble which turned out to be a broken USB cable but after replacing it, everything seemed ok.

The other thing I had to do on my Mac was to add the path to the platform-tools directory to the PATH in my .profile file. The easiest way to do this is to open a new terminal window and run the command:

open -a TextEdit .profile

This opens TextEdit with the .profile file loaded. Add in the following line somewhere replacing the placeholder with the path to your SDK installation folder:

# Add in Android tools
export PATH="/[PATH TO ANDROID SDK]/platform-tools":$PATH

You might be prompted to ‘unlock’ the file as you do this, just go with it. What this does is allow you to use the command line tools for accessing and controlling your Android device from a terminal on your development machine. To try it, open a new terminal window and type in:

adb devices

You should see your connected Andoid device listed in response.

In the next post I’ll describe how I went about creating the structure of the app and laying out the screen. If you’d like to go straight to the source code, I’ve uploaded it to GitHub and it can be found here: https://github.com/jpmasters/stem-ramp-configurator