This is a Google Daydream headset. It’s as good as new, and it’s e-waste.

Photograph of a Google Daydream headset’s box sitting on a table.

For those who don’t remember Daydream’s (very) brief history, here’s how it goes:

Cardboard was Google’s first mass-market VR product, and it was a hit! Google sold millions of inexpensive build-it-yourself VR headsets. Although Cardboard’s hardware was primitive, it jump-started a cottage industry of delightful little VR apps for Android.

Two years later, Google released Daydream, the supposed successor to Cardboard. For a variety of reasons, including not being made out of paper, Daydream was a huge step forward for Google’s budding VR ecosystem. Daydream smoothed over many of Cardboard’s rough edges, and provided a richer VR experience while remaining affordable.

Cardboard’s biggest shortcoming was its input device. Cardboard had a little button built into the headset itself, and that was the only input that came with it. Daydream did away with the button on the headset. Instead, Daydream came with a proper controller with multiple buttons, motion control, and a touchpad:

Photograph of a Google Daydream controller.

So, obviously, Daydream would be compatible with Cardboard, right? It’s just that the buttons were on the controller instead of the headset, right?

RIGHT!?!?

Of course not. If that were the case, I wouldn’t have anything to rant about.

You could not use a Daydream headset with a Cardboard VR app, and you could not use Cardboard with a Daydream app. The two products were totally incompatible. Even worse, Daydream could only be used with a small assortment of high-end Android phones. Cardboard played nice with nearly any contemporary Android phone. As a result, Daydream was largely rejected by the public and developers alike.

Google discontinued Daydream in late 2019, less than 3 years after release. Cardboard was discontinued in early 2021, having outlasted its supposed successor by 16 months. You cannot download Daydream apps from the Google Play Store anymore. Even if you could, the apps are incompatible with any modern version of Android. Daydream is, functionally, e-waste.

Daydream’s Legacy

Daydream is an archetypal example of great engineering and bad management.

In my opinion, the Daydream headset is comfortable, even with glasses. It feels nicely-made. Daydream addressed Cardboard’s greatest shortcomings in a cost-minded fashion.

Despite all the improvements over Cardboard, Daydream was still 5-10x cheaper than contemporary VR products like Oculus Rift and HTC Vive. Granted, it was cheaper because it required you to strap your phone to your face. But, having used a contemporary Vive headset, I was impressed by how much Daydream accomplished at such a low price point. It was 70% of the experience at 10% of the price.

Daydream could have been a good product, but it was doomed by objectively stupid product management decisions. It was a compatibility island, so nobody wanted to buy it. Since nobody bought it, nobody wanted to make apps for it. Even if cross-compatibility was a remotely difficult technical problem, Google is a juggernaut. They absolutely had the resources to figure it out. They chose not to, and so the world was left with a bunch of useless VR headsets. At least Cardboard was biodegradable.

Motivation

As an engineer and an unlucky owner of a Daydream headset, I have a moral obligation to turn this heap of plastic into something vaguely useful. I have no idea what to do with the headset itself, but I can certainly mess around with the controller!

Here’s a video of me controlling my PC with the Daydream controller. My Daydream2HID project turns any Google Daydream controller into a USB mouse. If you want to skip the reverse-engineering stuff and learn more about Daydream2HID, click here.

Scope

Before I start poking this thing, I want to set some goals:

  1. Figure out how to extract data from all the controller’s input methods. That includes all buttons, the trackpad, and motion sensor(s).
  2. Figure out how to reliably detect the controller’s presence, so that an open-source driver could be written for it.
  3. Keep the controller intact, so that it can be used for another project.

Non-goals, if I can avoid them:

  • Figure out how to flash the controller with new firmware.
  • Reverse-engineer the controller’s hardware.

Bluetooth Poking

Unsurprisingly, the Daydream controller is just a simple Bluetooth Low Energy (BLE) device. After all, there aren’t many ways to make a cheap peripheral that connects to a phone wirelessly.

I used the nRF Connect app to investigate the Daydream controller. First, I inspected the controller’s Bluetooth advertising data. If you aren’t familiar with Bluetooth terminology, an Advertisement is the stuff a Bluetooth device sends out when it’s ready for your phone/computer/whatever to pair with it (It’s actually more complicated than that, but bear with me).

The Daydream controller’s advertisements contain the following data:

  • A device name attribute, with the value Daydream controller in ASCII.
  • Two 16-bit service UUIDs, 0xFE55 and 0xFEF5.

It should be easy to pick out a Daydream controller in a crowd of various Bluetooth devices - just find the device whose advertisements contain the name “Daydream controller.” If you want to be extra strict, you could look for those two UUIDs, too.

There isn’t much else to look at in the advertisements, so I connected to the controller to inspect it further.

Here’s the complete process for pairing the controller with nRF Connect:

  1. Plug the controller into power, and charge it for half an hour or so.
  2. Unplug the controller (the next step will not work if it is plugged in).
  3. Press and hold the Home button until the white LED starts pulsing.
  4. Open the nRF Connect App. The controller will show up with the device name “Daydream controller”
  5. Connect to the controller, then bond with it.
  6. At this point, you can plug the controller back into power if you want. It will stay connected.

Once you have bonded with the controller, you have total control. There is no DRM, no stupid locks, nothing. You’re free to explore as you please.

GATT Crash Course

The Daydream controller is a GATT device. If you already know what GATT is, feel free to skip to the next section.

Bluetooth Low Energy (BLE) devices frequently use the Generic Attribute (GATT) profile. Think of a GATT “attribute” like a key-value pair. Every attribute has an ID, which is either 16 or 128 bits long, and a value, which can be any length within some technical limits.

16-bit IDs are written as a hex integer, for example 0x1234.

128-bit IDs are written in the usual UUID format, like c7b099c0-6232-4f3d-9d1a-0f7bd2caef14.

GATT calls these IDs “UUIDs,” so that’s what I will call them from now on.

Attributes have a variety of higher-order purposes. But, you only need to know two of them to understand the following writeup. There are two important types of GATT attributes: Services, and Characteristics.

A Characteristic represents a single attribute of the device. This could be its battery level, a sensor reading, or anything else really.

A Service groups together several characteristics. Every characteristic is a member of one service. So, you end up with a shallow tree-like hierarchy, like this:

There are three important verbs to know when talking about GATT: Read, Write, and Notify.

Read is when you request the value of a characteristic, and the device responds with the value.

Write is when you tell the device a new value for a characteristic.

Notify is when the device tells you the value of a characteristic, without you requesting it.

You can think of Read and Write like the usual verbs for a key-value store. They’re synchronous, and initiated by the client (that’s you).

Meanwhile, notifications are like a stream. They are initiated by the server (that’s the Daydream controller).

Some characteristics might be “read-only,” in other words, they only accept the Read verb. Others may be “read-write,” “write-notify,” and so on. You get the idea.

The Daydream Controller’s GATT Services

GATT Services

The controller exposes six GATT services, all using 16-bit UUIDs:

  • 0x1800: Bluetooth GAP protocol stuff. Nothing notable to see here.
  • 0x1801: Bluetooth GATT protocol stuff. Nothing notable to see here.
  • 0x180A: Standardized BLE service for reporting device info, like serial number, firmware version, and so on.
  • 0x180F: Standardized BLE service for reporting battery level.
  • 0xFEF5: Nonstandard service doing proprietary things. We’ll call this one “SUOTA Service.”
  • 0xFE55: Nonstandard service doing proprietary things. We’ll call this one “Google Service.”

Let’s go through some of the more important GATT services.

Battery Level

First, something mundane. Service 0x180A is the bog-standard Bluetooth GATT service for battery info. You can fetch the controller’s battery level by reading characteristic 0x2A19, which is part of this service. The battery level is returned as a percentage, encoded in one byte.

Device Information

Service 0x180F is the Bluetooth standardized “Device Information” service. The Daydream controller exposes the following information inside this service:

UUID Description Value
0x2A29 Manufacturer name Google Inc.
0x2A24 Model number Daydream controller
0x2A25 Serial number QSM65000855050D79S0163831A81MB0000
0x2A27 Hardware revision 0005.0003
0x2A26 Firmware revision 1.0.39
0x2A28 Software revision 825796af
0x2A50 PnP ID USB VID 0x18D1, PID 37392, Product Version 1

All of these are bog-standard Bluetooth GATT characteristics, and it appears that Google implemented them in conformance with the official spec.

The PnP ID one is intriguing. 0x18D1 is indeed Google’s official USB Vendor ID. If I had to guess, Google’s Daydream driver probably detects the controller by looking for this characteristic, and making sure the VID and PID match. Daydream apps are very hard to come by nowadays, so I can’t be sure about that.

SUOTA Service

The 0xFEF5 service is a funny one. 0xFEF5 is assigned as a “Member Service UUID” to Dialog Semiconductor, who was acquired by Renesas in 2021. It exposes the following characteristics, which all use 128-bit UUIDs:

UUID Verbs
8082caa8-41a6-4021-91c6-56f9b954cc34 Read/Write
724249f0-5ec3-4b5f-8804-42345af08651 Read/Write
6c53db25-47a1-45fe-a022-7c92fb334fd4 Read-only
9d84b9a3-000c-49d8-9183-855b673fda31 Read/Write
457871e8-d516-4ca1-9116-57d0b17b9cb2 Read/Write/Write No Response
5f78df94-798c-46f5-990a-b3eb6a065c88 Read/Notify

After searching around for these UUIDs, this service is almost certainly coming from an off-the-shelf OTA firmware update library called SUOTA. Jesus Bamford found the exact same GATT service being used in a Pokémon Go Plus wristband. Searches for the 128-bit UUIDs yield results for all sorts of seemingly-unrelated Bluetooth gadgets.

All of this suggests that the Daydream controller is based on a Renesas DA14xxx Bluetooth SoC. SUOTA appears to be specific to these chips, and the source code wasn’t readily available through a first party.

This information doesn’t get us any closer to meeting our three goals. However, it might be useful to some hacker out there, so here it is!

Google Service

Given that 0xFEF5 was Dialog’s Member Service UUID, you might not be surprised to learn that the other proprietary service UUID, 0xFE55, is assigned to Google!

This service exposes this very goofy-looking set of characteristics:

UUID Verbs
00000001-1000-1000-8000-00805f9b34fb Notify-only
00000002-1000-1000-8000-00805f9b34fb Write-only
00000003-1000-1000-8000-00805f9b34fb Read-only

We’ll call these characteristics 1, 2, and 3, matching the first digit part of UUID. This service holds the key to making the Daydream controller useful again.

Decoding the Google Service

I started with 3, the read-only characteristic. No matter what I did, it always responded with a two-byte value. With the remote sitting flat on a table, here are some of its responses:

35-0F
34-0F
31-0F
31-0F
31-0F
30-0F
30-0F
31-0F
31-0F
31-0F

I started moving the controller around, and I pressed some buttons, and the readings changed a little bit:

30-0F
2D-0F
2F-0F
31-0F
30-0F
2D-0F
2D-0F
2D-0F
2E-0F
2C-0F
2C-0F

Unsure of what to do at this point, I moved on.

Characteristic 1: The Key

As soon as I enabled notifications for Characteristic 1, a deluge of notifications poured in:

0B-83-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
10-87-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
29-0B-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
31-8F-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
41-93-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
49-17-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
61-9B-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
6A-1F-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
71-A3-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
7A-27-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
91-2B-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
99-AF-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
B2-33-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
B9-B7-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
C2-3B-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
D9-BF-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
E2-43-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
EA-C7-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-00
F2-4B-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01
0A-4F-FD-FC-B1-FE-BF-FE-44-16-00-A0-00-00-00-00-00-00-00-01

I started pressing buttons and moving the remote around, and the numbers started changing more erratically:

B5-70-03-5C-71-FE-BF-F6-04-16-00-A0-00-80-03-FE-60-00-00-01
BD-F4-03-7C-71-FE-AF-F5-44-16-00-A0-01-00-03-FD-40-00-00-01
C5-78-03-BC-71-FE-A7-F3-84-16-00-90-01-00-03-FD-20-00-00-01
CD-FC-03-DC-71-FE-9F-F3-C4-16-00-60-00-80-03-FE-60-00-00-01
E5-80-03-BC-71-FE-A7-F6-44-16-00-70-00-00-00-00-11-09-60-01
ED-04-03-DC-71-FE-9F-F5-44-16-00-60-00-80-03-FD-B0-E9-60-01
F5-88-03-FC-71-FE-8F-F2-C4-16-00-60-00-00-03-FD-F0-89-60-01
0E-0C-03-FC-72-FE-7F-F2-84-10-00-AF-FC-80-18-01-2D-89-40-01
15-90-03-9C-73-FE-97-F2-44-10-00-AF-FD-00-20-04-4D-09-60-01
1E-14-03-5C-73-FE-9F-F6-C4-10-00-AF-FF-00-14-03-6C-A9-80-01
25-98-03-7C-73-FE-9F-F7-04-16-00-A0-00-00-07-FF-2C-69-A0-01
3E-1C-04-7C-73-FE-5F-F2-84-16-00-60-01-FF-FB-FB-8C-49-A0-01
46-A0-04-7C-73-FE-5F-F0-C4-16-00-30-00-00-03-FF-4C-69-A0-01
E6-D4-05-FC-73-FD-D7-EE-84-10-00-A0-00-00-00-00-3E-6C-20-01
FF-58-05-DC-73-FD-D7-EE-84-10-00-60-00-00-00-00-5E-8C-20-01
9F-8C-06-5C-74-FD-9F-ED-84-0E-00-90-00-00-03-FE-AA-29-40-01
B8-10-06-5C-74-FD-9F-ED-04-10-00-60-00-00-00-01-06-C9-20-01
BF-14-06-3C-74-FD-A7-EE-04-10-00-6F-FF-80-00-02-06-29-20-01
C7-98-06-1C-74-FD-AF-EF-84-10-00-6F-FF-80-04-01-A5-C9-00-01
E0-1C-06-5C-75-FD-9F-ED-44-0E-00-90-00-FF-FF-FD-A5-09-00-01

Simultaneously excited by my discovery, and dumbfounded by the simplicity of the Daydream controller, I started trying to decode these packets.

Decoding

Okay, it’s Google, so it would make sense if it was Protobuf, right? Yes, but this data doesn’t smell like Protobuf to me. Protobuf mostly consists of key-value pairs of varints. The keys are usually small, too. So, encoded protobuf usually starts with a byte with a low value - say, 01, 1F or thereabouts. That’s followed by some gunk, then another low byte, then more gunk, and so on.

This data isn’t following that pattern. The first byte is all over the place, and there are sequences of zero bytes later on, too. Just to be sure, I tried decoding some of the notifications as protobuf, and sure enough, they returned junk.

Okay, let’s just take this thing apart manually instead.

Sitting flat on a table, only the first two bytes change. The other bytes are constant:

0B83 FDFCB1FEBFFE441600A00000000000000001
1087 FDFCB1FEBFFE441600A00000000000000001
290B FDFCB1FEBFFE441600A00000000000000001
318F FDFCB1FEBFFE441600A00000000000000001

If I spin the controller 180 degrees on the table, that mostly-zero area starts freaking out, too:

FC80 00DB3BFF67FAC41600D0027DE3FC80000001
0404 011B2EFF5FFB84160070027DD7FCA0000001
1C88 019B10FF4FF9441600A001FE97FEC0000001
250C 01DB06FF47F6C41600A0027E5BFDA0000001
2C90 01FAFBFF47F8841600D002FE2BFE80000001

Oh, interesting, part of that first 16-bit word looks like its counting up by four. If I mush buttons, it’s clear that the second-to-last byte is a bitfield of buttons:

Button Mask
Trackpad click 01
Home 02
App 04
Volume Down 08
Volume Up 10

Anywho, here’s where I’m at now:

                                              ,--- button bitmask
Time?     Gyro???      Accelerometer???       |  ,- always one?
FC80 00DB 3BFF 67FA C416 00D0 027D E3FC 8000 00 01

So, the buttons weren’t hard. Decoding the gyro and accelerometer was going to be more difficult, though. I put the controller in a vise so that I could move it around precisely, one axis at a time.

1 hour later

Oh Christ, it’s bit-packed.

I got about halfway through decoding the packet protocol when I thought to myself “hmmm, I wonder if someone has already done this.” As it turns out, there is indeed a Stack Overflow thread on this exact topic. The top response shows the exact encoding I found on my device, but there are a few issues:

  • Their remote returns a different value in the last byte. It’s not clear what this byte does.
  • They didn’t mention which gyro/accelerometer/orientation numbers corresponded to each physical axis.

So, I did get nerd-sniped, but only partially. Here’s a complete breakdown of the Daydream controller’s packet protocol:

The Google Daydream Controller Protocol

Here is the packet encoding:

struct protocol {
   UINT timestamp       : 9;
   UINT sqn             : 5;
   SINT orientation_x   : 13;
   SINT orientation_z   : 13;
   SINT orientation_y   : 13;
   SINT accel_x         : 13;
   SINT accel_z         : 13;
   SINT accel_y         : 13;
   SINT gyro_x          : 13;
   SINT gyro_z          : 13;
   SINT gyro_y          : 13;
   UINT trackpad_x      : 8;
   UINT trackpad_y      : 8;
   BIT  volume_up_btn   : 1;
   BIT  volume_down_btn : 1;
   BIT  app_btn         : 1;
   BIT  home_btn        : 1;
   BIT  trackpad_btn    : 1;
   UINT mystery         : 8;
};

Every packet is 20 bytes. On my controller, the final byte is always 01. This does not appear to be the case for other controllers, based on similar efforts I’ve seen online. I have no idea what this byte does, but it doesn’t appear to be doing anything important.

Here’s the packet protocol again, but pictorially:

Picture showing the bit-by-bit layout of the Daydream controller packets.

And here is how all of that maps to the physical world:

Picture of the Daydream controller with its three axes superimposed over it. Arrows point to each of the controller’s buttons.

Let’s walk through each of the fields in the protocol.

Timestamp

The timestamp field is a 9 bit unsigned integer. It counts upwards, and it’s measured in milliseconds.

Driver code can use the timestamp field to accurately determine the time interval between two consecutive packets. But, because timestamp overflows every 512 milliseconds, some special considerations need to be taken for this to work properly. Here is some pseudocode that determines the time elapsed between two consecutive packets:

function get_delta_t (PreviousPacket, Packet) =
   if Packet.timestamp <= PreviousPacket.timestamp
      return 512 - PreviousPacket.timestamp + Packet.timestamp
   else
      return Packet.timestamp - PreviousPacket.timestamp

When I tested my controller, the average interval between two packets was 25 milliseconds. The interval varied quite a bit, though, so these timestamps should definitely be used when doing precise motion vector calculations.

Sequence Number

The sqn field is a 5-bit unsigned integer. It monotonically counts upwards. Any two consecutive notifications will have consecutive sequence numbers - except when sqn reaches 31, at which point the next packet’s sqn will be zero.

I’m assuming this is meant to allow driver code to detect packet loss, though that’s a little odd considering that BLE’s link layer ensures reliable packet delivery. Maybe Google knew that Android’s broken-ass Bluetooth API was going to cause reliability problems. Hmm.

Orientation

orientation_x, orientation_z, and orientation_y contain the controller’s angular position.

These fields are 13-bit signed integer, so they could fit any value in the range (-4096, 4095). However, it appears that the controller only uses the range (-2048, 2047) for each orientation field.

The precision of these fields is 0.08789 degrees, or 0.00153 radians.

Google’s online instructions for the Daydream controller state that you can press and hold the Home button to reset the controller’s orientation. They must have been doing that in software, because holding the Home button seems to have no effect on the orientation fields in the packet.

Accelerometer

accel_x, accel_y, and accel_z contain the controller’s linear acceleration. These fields are 13-bit signed integers, and the controller uses the full (-4096, 4095) range of each field.

I don’t have an easy way to measure the precision of all the axes individually, but the Z axis averages 547 while sitting flat on a table. From that, I know the resolution of the Z-axis is 0.018 m/s2. I’d guess the other accelerometer axes have the same precision.

Gyroscope

gyro_x, gyro_y and gyro_z contain the controller’s angular velocity. These fields are 13-bit signed integers, and the controller appears uses the full (-4096, 4095) range of each field.

I’m not sure of the precision of these fields, since I don’t have a good way to measure it.

Trackpad

trackpad_x and trackpad_y show the position of the user’s finger on the trackpad. These are 8-bit unsigned integers.

When the user is not touching the trackpad, both fields are zero. When the user touches the trackpad, both fields become nonzero. When touching the trackpad, the smallest number I could get out of either axis was 9, and the largest was 247. I’d still recommend writing drivers with the assumption that the values for “touching” could be anything in the range (1, 255).

trackpad_x increases going left to right, and trackpad_y increases going bottom to top.

Buttons

These are pretty self-explanatory. 0 means the button isn’t pressed, 1 means the button is pressed.

Mystery Byte

The final byte of the packet is a mystery to me. Maybe it’s a protocol version flag? If so, it’s really strange that they put it at the end of the packet, instead of the start.

In my testing, this byte was always 01, no matter what I did. YMMV.

Daydream2HID

Last year, I wrote a quick post about a Lenovo mini PC with a Ryzen APU. Since then, that PC has lived under my TV - my wife and I use it to play casual games with my family.

As great as the little Lenovo box has been for this purpose, it definitely could be a better experience. I have one of those wireless keyboard-mouse combo things for controlling the TV. Although it does the job, it’s awkward to have to balance it on your legs while you’re laying on the couch. I thought about buying one of gyro “air mice,” but it would be hideously expensive for a device I wouldn’t use very much. So, I decided to make my own!

My Daydream2HID project turns any Google Daydream controller into a USB HID device. I’m running it on an nRF52840 dev board, but you could run it on just about any board that has a Bluetooth radio and USB. If you have your own nRF52840 dev kit, but you don’t feel like compiling the firmware from source, here’s a hex file. Have fun!

Daydream2HID is a pretty straightforward little chunk of embedded firmware. the src/ directory contains all the guts. Here’s a quick synopsis of each file:

  • bluetooth.c contains all the BLE code. It scans for a Daydream controller, pairs with the first controller it finds, and then it starts pumping the controller’s BT notifications into daydream.c.
  • daydream.c contains all the logic for decoding the controller’s packets. After decoding the packets, it feeds the decoded data into mouse.c.
  • mouse.c converts the decoded Daydream controller data into USB HID packets that emulate a mouse. If you want to adjust the controller’s sensitivity, or change the button mapping, this is the code you’ll want to play with.
  • usb_hid.c and usbd.c are mostly copy-pasted stuff from some Zephyr sample code. These files set up the USB port.
  • buttons.c is just a little bit of helper logic for dealing with the controller’s buttons.
  • leds.c controls the board’s status LEDs.

Daydream2HID uses Zephyr, which proved to be a great choice. Zephyr comes out-of-the-box with all the annoying low-level Bluetooth and USB stuff sorted out, which made life a lot easier.

mouse.c is pretty ugly right now. I might separate it out into two files, one with for the motion vectoring/state management stuff, and another file for encoding the HID packets. Other than that, I’m pretty satisfied with the state of this project.

At first, I was surprised at how easy it was to repurpose the Daydream controller. But, as I thought about it more, I realized there’s no good reason why the Daydream controller shouldn’t be usable this way. This is how all products would be designed, if not for the avaricious, anti-consumer design practices that we’ve come to expect from companies like Google. Daydream may be a failed product, but the industry could really learn something from it.

If you like Daydream2HID, consider buying me a coffee!