7

The daydream controller is awesome and we want to be able to use it in my AR app. It pairs via bluetooth to the HoloLens just fine, but not sure if I can view it in Unity.

Both HoloLens and daydream require their own Unity technical previews. The gvr Controller code is online but seems to speak directly to GVR C api.

Any thoughts on if accessing daydream controller in Unity outside the daydream tech preview is even possible?

Anand Agarawala
  • 111
  • 1
  • 3
  • 1
    Btw, Mr. Doob has built a JS version of @Dotphracker's amazing code answer provided below. https://github.com/mrdoob/daydream-controller.js/ – Anand Agarawala Feb 28 '17 at 19:20

1 Answers1

15

It is very possible to access the daydream controller without the GVR services. I am in fact working on that myself and can share what I know.

Getting the data

Using bluetooth gatt you can view all the data available and subscribe to the ID you want. I don't know how you would do this within Hololens/Unity specifically. Basically you want to:

  1. Connect to the device
  2. Choose the service (0000fe55-0000-1000-8000-00805f9b34fb)
  3. Choose the characteristic (00000001-1000-1000-8000-00805f9b34fb)
  4. Request notifications for it (00002902-0000-1000-8000-00805f9b34fb)

Android Example:

static final UUID DAYDREAM_CUSTOM_SERVICE = UUID.fromString("0000fe55-0000-1000-8000-00805f9b34fb");
static final UUID DAYDREAM_CHARACTERISTIC = UUID.fromString("00000001-1000-1000-8000-00805f9b34fb");
static final UUID CHARACTERISTIC_UPDATE_NOTIFICATION_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
...
BluetoothGattService service = gatt.getService(DAYDREAM_CUSTOM_SERVICE);
BluetoothGattCharacteristic characteristic = service.getCharacteristic(DAYDREAM_CHARACTERISTIC);
gatt.setCharacteristicNotification(characteristic, true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CHARACTERISTIC_UPDATE_NOTIFICATION_DESCRIPTOR_UUID);
descriptor.setValue( BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);

I suggest looking up Bluetooth Gatt to understand more about services and characteristics. I also used the BLE Scanner app on the playstore to view a lot of this information before starting with code.

Parsing the data

The device gives 20 bytes of data to work with. It is comprised of time, orientation, acceleration, raw gyro, touch position, and button flags.

Example (laying flat on a table):

5BEBFFB825FDB000041000B00000000000000000
63EFFFB825FDB000041000B00000000000000008
6C73FFB825FDB000041000B00000000000000038

Example (using touch pad):

480BFE87EB00E801841000B00000000191FBA008
4F8FFE47EB00E800441000B0000003FEB1FBA038
5893FE27EB00EFFF041000B0000003FF51FBA000

The byte definition is below:

Bytes:

  - 1: TTTT TTTT * T for time, loops
  - 2: TNNN NNKK * N is sequence number
  - 3: KKKK KKKK * IJK is orientation
  - 4: KKKI IIII
  - 5: IIII IIII
  - 6: JJJJ JJJJ
  - 7: JJJJ JOOO * MNO is acceleration
  - 8: OOOO OOOO
  - 9: OONN NNNN
  -10: NNNN NNNM
  -11: MMMM MMMM
  -12: MMMM CCCC * CDE for raw gyro
  -13: CCCC CCCC
  -14: CDDD DDDD
  -15: DDDD DDEE
  -16: EEEE EEEE
  -17: EEEX XXXX * All the X is the X touch position (8 bits)
  -18: XXXY YYYY * Y the Y touch position (8 bits)
  -19: YYYB BBBB * B the buttons (5 bits | [+][-][App][Home][Click])
  -20: Values vary

With this I have the touch pad and buttons able to work with any bluetooth device I can build apps for. In addition, you would need to add back functionality to reset the device position, control audio, etc.

Using this definition on Android:

static final int CLICK_BTN = 0x1;
static final int HOME_BTN = 0x2;
static final int APP_BTN = 0x4;
static final int VOL_DOWN_BTN = 0x8;
static final int VOL_UP_BTN = 0x10;
float xTouch=0, yTouch=0;
...
final boolean isClickDown = (data[18] & CLICK_BTN) > 0;
final boolean isHomeDown = (data[18] & HOME_BTN) > 0;
final boolean isAppDown = (data[18] & APP_BTN) > 0;
final boolean isVolMinusDown = (data[18] & VOL_DOWN_BTN) > 0;
final boolean isVolPlusDown = (data[18] & VOL_UP_BTN) > 0;

final int time = ((data[0] & 0xFF) << 1 | (data[1] & 0x80) >> 7 );

final int seq = (data[1] & 0x7C) >> 2;

int xOri = (data[1] & 0x03) << 11 | (data[2] & 0xFF) << 3 | (data[3] & 0xE0) >> 5;
xOri = (xOri << 19) >> 19;

int yOri = (data[3] & 0x1F) << 8 | (data[4] & 0xFF);
yOri = (yOri << 19) >> 19;

int zOri = (data[5] & 0xFF) << 5 | (data[6] & 0xF8) >> 3;
zOri = (zOri << 19) >> 19;

int xAcc = (data[6] & 0x07) << 10 | (data[7] & 0xFF) << 2 | (data[8] & 0xC0) >> 6;
xAcc = (xAcc << 19) >> 19;

int yAcc = (data[8] & 0x3F) << 7 | (data[9] & 0xFE) >>> 1;
yAcc = (yAcc << 19) >> 19;

int zAcc = (data[9] & 0x01) << 12 | (data[10] & 0xFF) << 4 | (data[11] & 0xF0) >> 4;
zAcc = (zAcc << 19) >> 19;

int xGyro = ((data[11] & 0x0F) << 9 | (data[12] & 0xFF) << 1 | (data[13] & 0x80) >> 7);
xGyro = (xGyro << 19) >> 19;

int yGyro = ((data[13] & 0x7F) << 6 | (data[14] & 0xFC) >> 2 );
yGyro = (yGyro << 19) >> 19;

int zGyro = ((data[14] & 0x03) << 11 | (data[15] & 0xFF) << 3 | (data[16] & 0xE0) >> 5);
zGyro = (zGyro << 19) >> 19;

xTouch = ((data[16] & 0x1F) << 3 | (data[17] & 0xE0) >> 5) / 255.0f;
yTouch = ((data[17] & 0x1F) << 3 | (data[18] & 0xE0) >> 5) / 255.0f;

This could be optimized but it assigns all the bits except for the last byte. The code value = (value << 19) >> 19 can also be value = (value >> 12) == 0 ? value : ~0x1FFF | value. It is just to extend the signed bit to a 32bit signed int.

I hope this helps and look forward to additional answers.

-- Update 2 --

After looking at the gvr code I found I had some issues with my previous assumptions. It's actually Orientation/Acceleration/Gyro. Also there was 1 more bit for sequence and 1 less for time. I've updated the byte definition and android example.

In addition the X,Y,Z values need to be scaled to floats. For Unity you could put the ints into Vector3s and then use the following. I also negated the x and y in oriVector, for Unity.

Vector3 oriVector = new Vector3 (-xOri, -yOri, zOri);
...
oriVector *= (2 * Mathf.PI / 4095.0);
accVector *= (8 * 9.8 / 4095.0);
gyroVector *= (2048 / 180 * Mathf.PI / 4095.0);

Then to get the rotation you just need the oriVector. Which is actually an axis-angle stored as: unit vector * angle.

public Quaternion orientation = Quaternion.identity;
private Quaternion controllerPoseInSensorSpace = Quaternion.identity;
private Quaternion startFromSensorTransformation = Quaternion.identity;
...
// do this bit after getting the data and scaling it
float sqrMagnitude = oriVector.sqrMagnitude;
if (sqrMagnitude > 0) {
    // extract radian angle
    float w = Mathf.Sqrt (sqrMagnitude);
    // normalize vector
    oriVector /= w;
    // set orientation space
    setOrientationInSensorSpace (w,oriVector);
}
...
// then assign to a Transform
controller.localRotation = this.orientation;
...
// sets orientation with rotation offset
void setOrientationInSensorSpace(float angle, Vector3 axis) {
    // set orientation space
    this.controllerPoseInSensorSpace = Quaternion.AngleAxis(angle*Mathf.Rad2Deg,axis);
    // rotate based on centered offset
    this.orientation = this.startFromSensorTransformation * this.controllerPoseInSensorSpace;
}
...
// after holding home for 600 milliseconds
private void setStartFromSensorTransformation() {
    Vector3 angles = this.controllerPoseInSensorSpace.eulerAngles;
    // reset rotation on Y
    this.startFromSensorTransformation.Set(0,Mathf.Sin(-angles.y * Mathf.Deg2Rad / 2f), 0, Mathf.Cos(angles.y * Mathf.Deg2Rad / 2f));
    // could also reset all, easier to work with
    //this.startFromSensorTransformation = Quaternion.Inverse (this.controllerPoseInSensorSpace);
}

That is everything related to getting daydream working with regular bluetooth devices. I also used the above C# code within Unity3D.

-- Update 1 --

Added a more complete byte definition. The values missing before were the gyro, magnetometer, and acceleration data. They each have three 13bit signed ints. There also seems to be a sequence number tucked in with the time bits.

Going forward

In order to use the device data with other platforms you would need to put the data through similar equations used for 9DoF/IMU devices. I don't have knowledge of exactly how to address this.

The last byte

This is likely reserved for flags and I'm not sure on the meaning but I have some findings to list. Version number is the firmware version of the controller.

1.0.10 (out of the box): 0xF0/0xF8
1.0.10 (previously used with gvr): 0x00/0x08/0x38/0x51
1.0.15: 0x00/0x70
Forrest Porter
  • 1,252
  • 10
  • 11
  • Thanks for sharing your findings! Would be great if you could share your code if you make anymore progress. – Anand Agarawala Nov 23 '16 at 03:37
  • Thanks for that! Have you made any progress regarding the byte definition? – Alexandre Bulté Nov 28 '16 at 13:15
  • Yes, I've updated the data and believe it is complete besides the last byte. To have it completely working outside of GVR will require a bit of work but there seem to be a few videos I have googled that show use of 9DoF/IMU devices (arduino at least), being used even within Unity. – Forrest Porter Nov 28 '16 at 19:59
  • 1
    Btw, @Dotphracker we are now starting to port your code to HoloLens since the built in input methods aren't very reliable. Do you have any updates since your last post that might be helpful? Or even some code/github link you could share? We'd be massively greatful. – Anand Agarawala Dec 10 '16 at 00:48
  • @Dotphracker good news, we are reading bytes on HoloLens! We're currently trying to parse the byte array we're getting. Not really able to understand much of what the values mean exactly. Here is an example of the byte array we're getting back. 118,19,225,25,31,23,88,35,194,153,237,112,13,127,184,8,0,0,0,0 Were you able to get the appropriate Vector3 values from this? – Anand Agarawala Dec 14 '16 at 01:01
  • @AnandAgarawala You need to extract the data from the bytes using the definition I've defined. I updated the answer to include an example. The values won't give you a Vector3 value to use. You need to use some sensor fusion algorithm I can't explain them myself but it would be similar to "IMU position tracking". – Forrest Porter Dec 20 '16 at 03:55
  • 1
    @Dotphracker thanks for updating your code! All very helpful. We are using the JS sensor fusion code [here](https://hackernoon.com/how-i-hacked-google-daydream-controller-c4619ef318e4) and still having some trouble. Curious if you could share the code for your entire project on github? – Anand Agarawala Dec 21 '16 at 02:10
  • And side note, this is probably the most detailed/best reply that exists on the entire internet to someones general question. You sir deserve a trophy! – Anand Agarawala Dec 21 '16 at 15:40
  • @AnandAgarawala The link helped me a lot and I got a working example. If anyone wants to find the relavant code after using jd-gui just search the files for the relavant service uuid (fe55). Updated answer should be complete now. – Forrest Porter Dec 25 '16 at 04:50
  • Your updated answer is amazing!! Thank you so much, will update our code accordingly. – Anand Agarawala Jan 09 '17 at 22:09
  • @AnandAgarawala As an example, this is how it runs on unity/hololens [[video link](https://www.youtube.com/watch?v=AimspCTdGuc)] using everything I defined in my answer. – Forrest Porter Jan 10 '17 at 21:16
  • @Dotphracker Can you please elaborate what rawOrientation is in your code ? – JibranAhmed Apr 12 '17 at 01:23
  • @jeniusj It was a copy and paste error from my code. I renamed it for answer to be clearer. Fixed the answer. – Forrest Porter Apr 18 '17 at 22:25
  • Please have a look on how do you parse your oriX. You've wrote `4: KKKI IIII`. The bitmask for the first 3 bits of a byte is 0xE0 but in your code is `int xOri = (data[1] & 0x03) << 11 | (data[2] & 0xFF) << 3 | (data[3] & 0x80) >> 5`. You use the bitmask 0x80. By the way [How I hacked Google Daydream controller](https://hackernoon.com/how-i-hacked-google-daydream-controller-c4619ef318e4) what you called _orientation_ is in this article the gyroscope. – Vertex Apr 29 '17 at 09:41
  • @Vertex you are correct. I use a nodejs piece that has 0xE0 but somehow it became 0x80 for my android/unity examples. I used what the google vr core states it as. That article does have orientation event in the picture of the JD-GUI code for what he states as magnetometer event. All that is really needed is the orientation part because it gives you the quaternion of the device without needing additional algorithms. – Forrest Porter May 04 '17 at 03:05