/** @file tools/sdk/java/Format.java @author Luke Tokheim, luke@motionnode.com @version 1.0 (C) Copyright GLI Interactive LLC 2007. All rights reserved. The coded instructions, statements, computer programs, and/or related material (collectively the "Data") in these files contain unpublished information proprietary to GLI Interactive LLC, which is protected by US federal copyright law and by international treaties. The Data may not be disclosed or distributed to third parties, in whole or in part, without the prior written consent of GLI Interactive LLC. The Data is provided "as is" without express or implied warranty, and with no claim as to its suitability for any purpose. */ package MotionNode.SDK; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; import java.util.Vector; /** The Format class methods read a single binary message from a MotionNode service and return an object representation of that message. This is layered (by default) on top of the {@link Client} class which handles the socket level binary message protocol.

Example usage:

   // Connect to port 32078 on localhost.
   Client client = new Client(null, 32078);
   while (true) {
     if (client.waitForData()) {
       while (true) {
         // Block on the open connection until a message comes in.
         {@link ByteBuffer} data = client.{@link Client#readData};
         if (null == data) {
           break;
         }

         // Create an associative container from the binary message.
	 Map preview = Format.Preview(data);
	 // Iterate through the container.
	 for (Map.Entry entry: preview.entrySet()) {
	   float[] euler = entry.getValue().getEuler();
	   System.out.println("Euler = [" + euler[0] + "," + euler[1] + "," + euler[2] + "]");
	 }
       }
     }
   }
   
*/ public class Format { /** MotionNode services send a list of data elements. The {@link Format} functions create a map from integer id to array packed data for each service specific format. This is an abstract base class to implement a single format specific data element. The idea is that a child class implements a format specific interface (API) to access individual components of an array of packed data. For example, the {@link PreviewElement} class extends this class and provides a {@link PreviewElement#getEuler} method to access an array of {x, y, z} Euler angles. */ abstract public class Element { /** Array of packed binary data for this element. If data is not null then it contains a sample for each of the N channels. */ private Vector data = null; /** Constructor is protected. Only allow child classes to call this. @param data array of packed data values for this Format::Element @param length valid length of the data array */ protected Element(Vector data, int length) { if (data.size() == length) { this.data = data; } } /** Utility function to copy portions of the packed data array into its component elements. Only classes extending Element should call this method. @param base starting index to copy data from the internal data array @param length number of data values in this component element @return an array of element_length values, assigned to {m_data[i] ... m_data[i+element_length]} if there are valid values available or null otherwise @see #getShortData */ protected float[] getFloatData(int base, int length) { float[] result = new float[length]; if ((null != data) && (base + length <= data.size())) { for (int i=0; i should call this method. @param base starting index to copy data from the internal data array @param length number of data values in this component element @return an array of element_length values, assigned to {m_data[i] ... m_data[i+element_length]} if there are valid values available or null otherwise @see #getFloatData */ protected short[] getShortData(int base, int length) { short[] result = new short[length]; if ((null != data) && (base + length <= data.size())) { for (int i=0; iN Preview data elements. Use this class to wrap a single Preview data element such that we can access individual components through a simple API.

Local orientation is defined relative to the start pose. The start pose is defined at take intitialization or by the user.

Preview element format:
id => [global quaternion, local quaternion, local euler, local translation]
id => {Gqw, Gqx, Gqy, Gqz, Lqw, Lqx, Lqy, Lqz, rx, ry, rz, tx, ty, tz}

*/ public class PreviewElement extends Element { /** Total number of channels in the packed data array. 2 quaternions, 1 set of Euler angles, 1 set of translation channels. */ public static final int Length = 2*4 + 2*3; /** Initialize this container identifier with a packed data array in the Preview format. @param data is a packed array of global quaternion, local quaternion, local Euler angle, and local translation channel data */ public PreviewElement(Vector data) { super(data, Length); } /** Get a set of x, y, and z Euler angles that define the current local orientation. Specified in radians assuming x-y-z rotation order. Not necessarily continuous over time, each angle lies on the domain [-pi, pi].

Euler angles are computed on the server side based on the current local quaternion orientation.

@return a three element array {x, y, z} of Euler angles in radians or null if there is no available data @see #getQuaternion */ public float[] getEuler() { return getFloatData(8, 3); } /** Get a 4-by-4 rotation matrix from the current global or local quaternion orientation. Specified as a sixteen element array in row-major order. @param local set to true get the local orientation, otherwise returns the global orientation @return a sixteen element array that defines a 4-by-4 transformation matrix in row-major order or the idenitity if there is no available data @see #getQuaternion */ public float[] getMatrix(boolean local) { return quaternion_to_R3_rotation(getQuaternion(local)); } /** Get the global or local unit quaternion that defines the current orientation. @param local set to true get the local orientation, otherwise returns the global orientation @return a four element array {w, x, y, z} that defines a unit length quaternion q = w + x*i + y*j + z*k or zeros if there is no available data */ public float[] getQuaternion(boolean local) { if (local) { return getFloatData(4, 4); } else { return getFloatData(0, 4); } } /** Get x, y, and z of the current estimate of linear acceleration. Specified in g. @return a three element array {x, y, z} of linear acceleration channels specified in g or zeros if there is no available data */ public float[] getAccelerate() { return getFloatData(11, 3); } } // class PreviewElement /** The Sensor service provides access to the current un-filtered sensor signals in real units. The Sensor service sends a map of N data elements. Use this class to wrap a single Sensor data element such that we can access individual components through a simple API. Sensor element format: id => [accelerometer, magnetometer, gyroscope] id => {ax, ay, az, mx, my, mz, gx, gy, gz} */ public class SensorElement extends Element { public final static int Length = 3*3; /** Initialize this container identifier with a packed data array in the Sensor format. @param data is a packed array of accelerometer, magnetometer, and gyroscope un-filtered signal data. */ public SensorElement(Vector data) { super(data, Length); } /** Get a set of x, y, and z values of the current un-filtered accelerometer signal. Specified in g where 1 g = -9.8 meters/sec^2. Domain varies with configuration. Maximum is [-6, 6] g. @return a three element array {x, y, z} of acceleration in gs or zeros if there is no available data */ public float[] getAccelerometer() { return getFloatData(0, 3); } /** Get a set of x, y, and z values of the current un-filtered gyroscope signal. Specified in degrees/second. Valid domain is [-500, 500] degress/second. @return a three element array {x, y, z} of angular velocity in degrees/second or zeros if there is no available data */ public float[] getGyroscope() { return getFloatData(6, 3); } /** Get a set of x, y, and z values of the current un-filtered magnetometer signal. Specified in uT (microtesla). Domain varies with local magnetic field strength. Expect values on [-60, 60] uT (microtesla). @return a three element array {x, y, z} of magnetic field strength in uT (microtesla) or zeros if there is no available data */ public float[] getMagnetometer() { return getFloatData(3, 3); } } // class SensorElement /** The Raw service provides access to the current uncalibrated, unprocessed sensor signals in signed integer format. The Raw service sends a map of N data elements. Use this class to wrap a single Raw data element such that we can access individual components through a simple API.

Raw element format:
id => [accelerometer, magnetometer, gyroscope]
id => {ax, ay, az, mx, my, mz, gx, gy, gz}

All sensors output 12-bit integers. Process as 16-bit short integers on the server side.

*/ public class RawElement extends Element { public final static int Length = 3*3; /** Initialize this container identifier with a packed data array in the Raw format. @param data is a packed array of accelerometer, magnetometer, and gyroscope unprocessed signal data */ public RawElement(Vector data) { super(data, Length); } /** Get a set of x, y, and z values of the current unprocessed accelerometer signal.

Valid domain is [0, 4095].

@return a three element array {x, y, z} of raw accelerometer output or zeros if there is no available data */ public short[] getAccelerometer() { return getShortData(0, 3); } /** Get a set of x, y, and z values of the current unprocessed gyroscope signal.

Valid domain is [0, 4095].

@return a three element array {x, y, z} of raw gyroscope output or zeros if there is no available data */ public short[] getGyroscope() { return getShortData(6, 3); } /** Get a set of x, y, and z values of the current unprocessed magnetometer signal.

Valid domain is [0, 4095].

@return a three element array {x, y, z} of raw magnetometer output or zeros if there is no available data */ public short[] getMagnetometer() { return getShortData(3, 3); } } // class RawElement /** Create a map of N Preview data elements from a packed binary input message. Each of the N elements has an integer id and an array of M floating point values. @param buffer raw binary input message, for example directly from a call to {@link Client#readData} @return a {@link java.util.Map} collection representation of the input message @throws BufferUnderflowException if there are not enough bytes in the incoming buffer to complete the output object */ public static Map Preview(ByteBuffer buffer) throws BufferUnderflowException { Map result = new TreeMap(); { // Use this to do most of the dirty work. Map > map = IdToFloatArray(buffer, PreviewElement.Length); if (!map.isEmpty()) { // Need this to instance the PreviewElement class. final Format format = new Format(); //Iterator itr = map.entrySet().iterator(); //while (itr.hasNext()) { // Map.Entry entry = (Map.Entry)itr.next(); // result.put((Integer)entry.getKey(), // format.new PreviewElement((Vector)entry.getValue())); //} for (Map.Entry > entry: map.entrySet()) { result.put(entry.getKey(), format.new PreviewElement(entry.getValue())); } if (map.size() != result.size()) { result.clear(); } } } return result; } /** Create a map of N Sensor data elements from a packed binary input message. Each of the N elements has an integer id and an array of M floating point values. @param buffer raw binary input message, for example directly from a call to {@link Client#readData} @return a {@link java.util.Map} collection representation of the input message @throws BufferUnderflowException if there are not enough bytes in the incoming buffer to complete the output object */ public static Map Sensor(ByteBuffer buffer) throws BufferUnderflowException { Map result = new TreeMap(); { // Use this to do most of the dirty work. Map > map = IdToFloatArray(buffer, SensorElement.Length); if (!map.isEmpty()) { // Need this to instance the SensorElement class. final Format format = new Format(); //Iterator itr = map.entrySet().iterator(); //while (itr.hasNext()) { // Map.Entry entry = (Map.Entry)itr.next(); // result.put((Integer)entry.getKey(), // format.new SensorElement((Vector)entry.getValue())); //} for (Map.Entry > entry: map.entrySet()) { result.put(entry.getKey(), format.new SensorElement(entry.getValue())); } if (map.size() != result.size()) { result.clear(); } } } return result; } /** Create a map of N Sensor data elements from a packed binary input message. Each of the N elements has an integer id and an array of M floating point values. @param buffer raw binary input message, for example directly from a call to {@link Client#readData} @return a {@link java.util.Map} collection representation of the input message @throws BufferUnderflowException if there are not enough bytes in the incoming buffer to complete the output object */ public static Map Raw(ByteBuffer buffer) throws BufferUnderflowException { Map result = new TreeMap(); { // Use this to do most of the dirty work. Map > map = IdToShortArray(buffer, RawElement.Length); if (!map.isEmpty()) { // Need this to instance the SensorElement class. final Format format = new Format(); //Iterator itr = map.entrySet().iterator(); //while (itr.hasNext()) { // Map.Entry entry = (Map.Entry)itr.next(); // result.put((Integer)entry.getKey(), // format.new SensorElement((Vector)entry.getValue())); //} for (Map.Entry > entry: map.entrySet()) { result.put(entry.getKey(), format.new RawElement(entry.getValue())); } if (map.size() != result.size()) { result.clear(); } } } return result; } /** Create a map of N elements from a packed byte array. Each element has an id and a array of M elements. The incoming byte buffer must have exactly the number of bytes to complete N elements. N is computed based on the size of the input buffer and the length of each float array. @param buffer raw binary message input, for example directly from a call to {@link Client#readData} @param length the number of values in a single element's float array @return a {@link java.util.Map} > collection representation of the input message @throws BufferUnderflowException if there are not enough bytes in the incoming buffer to complete the output object */ private static Map > IdToFloatArray(ByteBuffer buffer, int length) throws BufferUnderflowException { // Use the sorted TreeMap, behaves like the C++ std::map. Map > result = new TreeMap >(); // Compute the size in bytes of a single element. Conside the input // buffer to be a packed set of element entries. final int element_size = (Integer.SIZE / Byte.SIZE) + (Float.SIZE / Byte.SIZE) * length; // while we have enough bytes to create a complete element. while (element_size <= buffer.remaining()) { Integer key = new Integer(buffer.getInt()); Vector value = new Vector(); for (int i=0; iN short integer elements from a packed byte array. Each element has an id and a array of M elements. The incoming byte buffer must have exactly the number of bytes to complete N elements. N is computed on the fly based on the size of the input buffer and the length of each short integer array. @param buffer raw binary message input, for example directly from a call to {@link Client#getSample} @param length the number of values in a single element's short integer array @return a {@link java.util.Map} > collection representation of the input message @throws BufferUnderflowException if there are not enough bytes in the incoming buffer to complete the output object */ private static Map > IdToShortArray(ByteBuffer buffer, int length) throws BufferUnderflowException { // Use the sorted TreeMap, behaves like the C++ std::map. Map > result = new TreeMap >(); // Compute the size in bytes of a single element. Conside the input // buffer to be a packed set of element entries. final int element_size = (Integer.SIZE / Byte.SIZE) + (Short.SIZE / Byte.SIZE) * length; // while we have enough bytes to create a complete element. while (element_size <= buffer.remaining()) { Integer key = new Integer(buffer.getInt()); Vector value = new Vector(); for (int i=0; iq = w + x*i + y*j + z*k = (w, x, y, z) @return an array of 16 elements in row-major order that defines a 4-by-4 rotation matrix computed from the input quaternion or the identity matrix if the input quaternion has zero length */ private float[] quaternion_to_R3_rotation(float[] q) { if ((null == q) || (4 != q.length)) { return null; } final float a = q[0]; final float b = q[1]; final float c = q[2]; final float d = q[3]; final float aa = a*a; final float ab = a*b; final float ac = a*c; final float ad = a*d; final float bb = b*b; final float bc = b*c; final float bd = b*d; final float cc = c*c; final float cd = c*d; final float dd = d*d; final float norme_carre = aa+bb+cc+dd; // Defaults to the identity matrix. float[] result = new float[16]; for (int i=0; i<4; i++) { result[4*i+i] = 1f; } if (norme_carre > 1e-6) { result[0] = (aa+bb-cc-dd)/norme_carre; result[1] = 2*(-ad+bc)/norme_carre; result[2] = 2*(ac+bd)/norme_carre; result[4] = 2*(ad+bc)/norme_carre; result[5] = (aa-bb+cc-dd)/norme_carre; result[6] = 2*(-ab+cd)/norme_carre; result[8] = 2*(-ac+bd)/norme_carre; result[9] = 2*(ab+cd)/norme_carre; result[10] = (aa-bb-cc+dd)/norme_carre; } return result; } /** Hide the constructor. There is no need to instantiate the Format object. */ private Format() { } /** Example usage and test function for the Format class. */ public static void main(String [] args) { ByteBuffer preview_data = ByteBuffer.allocate( (Integer.SIZE / Byte.SIZE) + (Float.SIZE / Byte.SIZE) * PreviewElement.Length); { preview_data.putInt(1); for (int i=0; i entry: preview.entrySet()) { // System.out.println("[" + entry.getKey() + "] => " + entry.getValue().getEuler()[0]); //} } } // class Format