Last week was mostly milights, and this week was even more milights. I got a new remote and wifi bridge that I'm hoping to use to finish descrambling the protocol, but in the mean time I wanted to use the wifi bridge to do some fun stuff. So mostly this was code to get colour data from OS X onto my lights.
The first thing I wanted was to match the colour temperature of my lights to that of my screen. I use f.lux to set the colour temperature for my monitors, so I was hoping to get the information out of that. Unfortunately, f.lux doesn't have an API, so I went looking for ways to get the information from OS X directly. My first attempt was to read the ICC profile out of the display using Python/PyObjC and parse it using PIL's ICC support. Unfortunately, the image profile didn't seem to contain the information I was looking for.
Attempt 2 involved a lot of looking through ICC data. I eventually realised that what I was looking for wasn't the "media white point" or the "chromatic adaptation", which are both ways in which the white point can be set, but another, third way, called the "vcgt", or Video Card Gamma Table. I pulled a parser with vcgt support out of pypng, used that to read the data, had to parse the data manually anyway (there are two different kinds, table-based and formula-based), but finally I had my data. I had to convert the rgb values to color temperatures, which I did using Python's super complicated color library. I think there's probably an easier way to do that, but it worked well enough and I managed to get a value I could pipe to another command which set the display temperature. The updating was still a bit strange, though, I think because f.lux doesn't always update the ICC profile when it makes changes.
Cleanup time. I suspected that there was a way to get the vcgt without having to parse the ICC data. I eventually found CGGetDisplayTransferByTable. There's also CGGetDisplayTransferByFormula but it straight up doesn't work. I only wanted the max gamma values anyway, so the table version was fine. I then proceeded as before but, to cut down on Python's ferocious startup time, I made the scripts stream updates instead of sending one per invocation. Finally, I could click around in f.lux and watch my lights update. Victory!
Fresh off my high from making colour temperatures work, I forged ahead with part 2 of the plan: getting RGB values from my monitor onto my lights. With a bit more background in using PyObjC it was easier this time, though I still spent a while figuring out how to make graphics contexts work. My approach was to take a screenshot, draw it onto a 1x1 canvas, and then read the pixel data from that canvas. This worked really well, and was fast enough to pull data at close to realtime when I tested it. I actually ran into more issues with the lights than with OS X, because they have a fairly limited bandwidth via the bridge. I added some optimisations to avoid resending repeated colour commands and that helped a bit. Also the hue settings on the lights are, uh, only vaguely related to actual hues. I ended up doing a quadratic regression to approximate what the colours should actually look like, which worked okay but I probably need to do it again with more data. Still, broadly speaking, it was a success.