A SunSPOT video game controller.
Ok. I'll admit it. The Wii game controller looks like good fun. Only I cannot bear myself to play Wii Sports. So I have no choice but create a movement based game controller by my own. Using those very nice SunSPOTs of course ![]()
SunSPOT video game controller - The Plan
|
22 July 2007
Ok. Before anything useful can be read from the accelerometer it must be properly calibrated. In the puzzle game calibration on one axis was enough but for this we need a little more accuracy. We'll need to determine the zero and one G values for each axis so that we can accurately determine when the SunSPOT is not accelerating (e.g. the Math.sqrt(x*x+y*y+z*z)==1). We need this to determine the orientation of the spot.
The following bit of code calibrates one of the axis of the accelerometer using gravity. To determine if the direction of the gravity is inline with the axis of the accelerometer we cheat a little using the magic 550 value. This value is about half a G and makes sure calibration is only done when the user holds the Spot correctly. The Leds are used to indicate the axis to the user.
private double[] zeroOffset = {465.5,465.5,465.5}; // default zero offset for raw accelerator value private double[] oneOffset = { 600.0,600.0,600.0}; // default one g offset /** performs calibrating on one axis (0=x,1=y,2=z) */ private void calibrate(int ax) { double[] d=new double[3]; double[] last=new double[3]; double delta=0; boolean positionGood=false; boolean stable=false; // leds[ax].setRGB(255,0,0); leds[ax].setOn(); // do try { // // Read the values and see if the gravity is on the correct axis. // positionGood=true; for (int t=0;t<3;t++) { d[t]=axis[t].getValue(); if (t==ax) { if (d[t]<550.0) { positionGood=false; } } else { if (d[t]>550.0) { positionGood=false; } } } // // See if the measurements are steady. // delta=0; for (int t=0;t<3;t++) delta=delta+Math.abs(d[t]-last[t]); stable=delta<=1; // // Store current values as previous value for the next round. // for (int t=0;t<3;t++) last[t]=d[t]; // Utils.sleep(100); // // Blink while calibrating // leds[ax].setOn(!leds[ax].isOn()); // } catch (IOException ex) { stable=false; } while ((!stable)||(!positionGood)); // // Store the values. One G value for the current axis, zero G value for // the other axis. // for (int t=0;t<3;t++) { if (ax==t) oneOffset[t]=d[t]; else zeroOffset[t]=d[t]; } // leds[ax].setOff(); }
Now that we know the values of one G and zero G on all axis, we can also calculate the sensitivity of the accelerometer on each axis:
private double[] sensitivity = {186.2,186.2,186.2}; protected void calculateSensitivity() { for (int t=0;t<3;t++) { sensitivity[t]=oneOffset[t]-zeroOffset[t]; } }
Good, now that we can read the calibrated values it is time for the next step.
6 Degrees of freedom, only 3 input variables..
If you move an object around you can translate it over its 3 axis (x,y,z) but you can also rotate it over any axis (roll,pitch,yaw). You cannot compute all these values using only one accelerometer. But you can cheat by calculating the rotation using gravity when the Spot is not accelerating and the translation when it is accelerating. The catch is of course you cannot move and rotate at the same time. Well.. we will worry about that later ![]()
29-07-2007 : Don't worry about the math, start with getting the data across.
Ok. Now that we have the spot calibrated in all 3 axis, it is time to transmit useful data to the PC so that we can actually do something with it. Of course we could use proper client-server architecture, but its easier just to broadcast the data (we may then even control multiple games at
the same time with a single controller
. The connection is obtained using the Connector class and were broadcasting on port 42 (I wonder why..)
private DatagramConnection output; private Datagram message; protected void setupRadio() throws IOException { output = (DatagramConnection)Connector.open("radiogram://broadcast:42"); message=output.newDatagram(output.getMaximumLength()); }
The actual sending of the data is rather straight forward because the Datagram class has many useful read and write methods. First the message is reset to clear any old data. Then the current
time is written (needed if we want to do some time based calculations later) and the value of
the accelerometer. Two boolean values are the state of the two buttons. Finally we send the message.
protected void transmit(double[] d,boolean b1,boolean b2) throws IOException { message.reset(); message.writeLong(System.currentTimeMillis()); message.writeInt(d.length); for (int t=0;t<d.length;t++) { message.writeDouble(d[t]); } message.writeBoolean(b1); message.writeBoolean(b2); output.send(message); }
So now we can send the data, which is good but we still need to get the corrected data from the accelerometer, which is done in the following method.
private double[] getAcceleration(double[] d) { if (d==null) d=new double[3]; for (int t=0;t<3;t++) try { d[t]=(axis[t].getValue()-zeroOffset[t])/sensitivity[t]; } catch (IOException ex) { d[t]=0; } return d; }
Okido. Now the thing that glues the whole together.. First we'll do the calibration, then the radio setup and then we'll enter the time compensated main loop which will (currently) just read the values and transmit them to the PC. There is a top-secret fail() which will turn all the leds to red if something goes wrong.
private IAccelerometer3D accel = EDemoBoard.getInstance().getAccelerometer(); private IScalarInput[] axis = new IScalarInput[] { accel.getXAxis(), accel.getYAxis(), accel.getZAxis()}; private ITriColorLED [] leds = EDemoBoard.getInstance().getLEDs(); private ISwitch[] buttons=EDemoBoard.getInstance().getSwitches(); protected void startApp() throws MIDletStateChangeException { double[] acceleration=new double[3]; // // Calibration // calibrate(2); calibrate(0); calibrate(1); calculateSensitivity(); try { // // Radio // setupRadio(); // // Main loop // long rt=System.currentTimeMillis(); long dt; while (true) { // // Get the acceleration // acceleration=getAcceleration(acceleration); // // Send the acceleration and the button states. // transmit(movement,buttons[0].isClosed(),buttons[1].isClosed()); // // Make sure the updates occur at a regular interval // In this case 100 ms (10 times per second). // dt=System.currentTimeMillis(); if ((dt-rt)<100) Utils.sleep(100-(dt-rt)); rt=System.currentTimeMillis(); // } } catch (Exception ex) { // ex.printStackTrace(); // // Give the user some visual indication that something has gone wrong. // fail(); } }
The PC Client and using the data to control a game.
The PC Client we're going to build now will be a very simple one, it will read the accelerometer data and use the rotation on the X and Y axis to position the mouse. With this it should be possible to control a simple mouse controlled game like echoes
.
The first step is to setup the Radio BaseStation to receive radiograms on port 42. Then an instance of the Robot class is created. Robot is a special utility in the awt package with which you can simulate user behaviour like moving the mouse or pressing buttons. Its useful for automated GUI testing but also for playing games with the SPOT
Finally a infinite loop is entered that just reads a packet, extracts the data and moves the mouse accordingly.
public static void main(String[] args) throws Exception { // // Set up radio // LowPanPacketDispatcher.getInstance().initBaseStation(); RadiogramConnection read = (RadiogramConnection)Connector.open("radiogram://:42"); read.setTimeout(10000); Datagram dg = read.newDatagram(read.getMaximumLength()); // // Setup the java.awt.Robot // Robot r=new Robot(); boolean lb1=false; boolean lb2=false; while(true) try { // // receive a datagram and extract the data. // read.receive(dg); long t=dg.readLong(); dg.readInt(); // double d1=dg.readDouble(); double d2=dg.readDouble(); double d3=dg.readDouble(); boolean b1=dg.readBoolean(); boolean b2=dg.readBoolean(); // // SPOT Button 2 enables/disables control over the mouse // (otherwise its so hard to regain control over the PC :-) // if ((b2)&&(!lb2)) { enabled=!enabled; } lb2=b2; // // If enabled, use data to drive the Robot // if (enabled) { // // Button one does mouse clicks. // if ((b1)&&(!lb1)) { r.mousePress(InputEvent.BUTTON1_MASK); } else if ((!b1)&&(lb1)) { r.mouseRelease(InputEvent.BUTTON1_MASK); } lb1=b1; // // Actual mouse movement on a 1024x800 screen. // r.mouseMove(512+(int)(512*(d1)),400-(int)(400*(d2))); // } } catch (TimeoutException ex) { // Try again. } }
Ooh. Thats it. Now lets see if we can play a game
with it ![]()
09 Aug 2007 : The upgrade to the Orange SDK.
Started out very well with the SunSPOT manager easily downloading and installing the new version and upgrading the firmware of the Spots. Very nicely done! There is one catch though.. a new version, a new API - the Accelerometer API is all changed ! Oh well. Maybe this is
the excuse I've been waiting for to make a better version
Installation on Vista.
So far I've been developing on my laptop. But for the next incarnation of the game controller I want to play a 'real' game - something my laptop cant deal with. Thats why I have a totally overspecced Vista machine, to play games. So I tried to install the Orange SDK on it using the SunSPOT manager. Whoops. That didn't work.. So I ended up copying the SDK from my laptop and manually adjusting the '.sunspot.properties' file in my home directory. That got the SDK working. But then there is a second catch. The SunSPOT device driver (actually a Virtual Serial Port for a USB device). The .inf file does not work on Vista. Fortunately someone found a workaround
. Hooray !
Read more about the SunSPOT Game Controller in EriksSPOTControllerV2.