Experimenting With Light On Apple Notebook Computers

© Amit Singh. All Rights Reserved. Written in June 2006

In February 2005, I wrote about using the sudden motion sensor in Apple notebook computers. The article sparked enough interest that I was compelled to follow up with the AMS2HID application that allowed motion sensor readings to be translated to key-presses and mouse movements. Consequently, the motion sensor could be "used with" arbitrary applications. As I wrote my Mac OS X Internals book (which is shipping now) over the last year and a half, I often received questions about "using" other sensors in Apple computers. In particular, people seemed most interested in experimenting with the ambient light sensor in Apple notebook computers. This document is a long overdue response to those queries.

There Are No Published Motion/Light Sensor APIs

Please note that there are no Apple "APIs" to access the motion sensor or the ambient light sensor. Contrary to the amazingly popular misconception, it is no more difficult to do this sort of programming on any other platform than Apple's. See Mark A. Smith's The ThinkPad APS Accelerometer Interface and SDL Modifications for Accelerometer Game Control, for example.

In this discussion, we will look at experimental source code for programmatically performing the following operations on a MacBook Pro.

Please note that I have only run these programs on a MacBook Pro (all except the one for manipulating a display's brightness, which should work on all models). They might run on other models, perhaps even without any modifications, or by just changing "AppleLMUController" to "IOI2CDeviceLMU" in the code.

Our approach will be a typical example of a user program's communication with the I/O Kit. We will open a connection to a specific I/O Kit service (called "AppleLMUController" in this case, or "IOI2CDeviceLMU" in the case of certain PowerBook G4 models). Thereafter, we will call one of the IOConnectMethod* interface functions: IOConnectMethodScalarIScalarO() specifically.

#include <IOKit/IOKitLib.h> kern_return_t IOConnectMethodScalarIScalarO(io_connect_t connect, unsigned int index, IOItemCount scalarInputCount, IOItemCount scalarOutputCount, ...);

Figure 1. The IOConnectMethodScalarIScalarO() I/O Kit library function

IOConnectMethodScalarIScalarO() is a variable-argument function whose inputs and outputs are both scalars, as its name implies. The connect argument represents a connection to an I/O Kit service object. We will obtain such a connection by calling IOServiceOpen(). The index argument to IOConnectMethodScalarIScalarO() specifies which of the possibly many functions supported by the service is to be invoked. The scalarInputCount and scalarOutputCount arguments specify the number of input and output arguments, respectively, that follow. Note that a count can be zero, in which case no respective arguments will follow.

The enumeration in Figure 2 shows index values for the functions that we are interested in.

// lmucommon.h #ifndef LMUCOMMON_H #define LMUCOMMON_H enum { kGetSensorReadingID = 0, // getSensorReading(int *, int *) kGetLEDBrightnessID = 1, // getLEDBrightness(int, int *) kSetLEDBrightnessID = 2, // setLEDBrightness(int, int, int *) kSetLEDFadeID = 3, // setLEDFade(int, int, int, int *) // other firmware-related functions // verifyFirmwareID = 4, // verifyFirmware(int *) // getFirmwareVersionID = 5, // getFirmwareVersion(int *) // other flashing-related functions // ... }; #endif

Figure 2. Some function-index values for the AppleLMUController service

With this background, let us look at a source code example for a program that periodically retrieves readings from the ambient light sensor. The program is crude, but it can be readily compiled and run. Figure 3 shows the source for lmutracker.c.

// lmutracker.c // // gcc -o lmutracker lmutracker.c -framework IOKit -framework CoreFoundation #include <mach/mach.h> #include <IOKit/IOKitLib.h> #include <CoreFoundation/CoreFoundation.h> #include "lmucommon.h" static double updateInterval = 0.1; static io_connect_t dataPort = 0; void updateTimerCallBack(CFRunLoopTimerRef timer, void *info) { kern_return_t kr; IOItemCount scalarInputCount = 0; IOItemCount scalarOutputCount = 2; SInt32 left = 0, right = 0; kr = IOConnectMethodScalarIScalarO(dataPort, kGetSensorReadingID, scalarInputCount, scalarOutputCount, &left, &right); if (kr == KERN_SUCCESS) { printf("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b%8ld %8ld", left, right); return; } if (kr == kIOReturnBusy) return; mach_error("I/O Kit error:", kr); exit(kr); } int main(void) { kern_return_t kr; io_service_t serviceObject; CFRunLoopTimerRef updateTimer; // Look up a registered IOService object whose class is AppleLMUController serviceObject = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleLMUController")); if (!serviceObject) { fprintf(stderr, "failed to find ambient light sensor\n"); exit(1); } // Create a connection to the IOService object kr = IOServiceOpen(serviceObject, mach_task_self(), 0, &dataPort); IOObjectRelease(serviceObject); if (kr != KERN_SUCCESS) { mach_error("IOServiceOpen:", kr); exit(kr); } setbuf(stdout, NULL); printf("%8ld %8ld", 0L, 0L); // Set up the loop and let it run... updateTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + updateInterval, updateInterval, 0, 0, updateTimerCallBack, NULL); CFRunLoopAddTimer(CFRunLoopGetCurrent(), updateTimer, kCFRunLoopDefaultMode); CFRunLoopRun(); exit(0); }

Figure 3. Retrieving ambient light sensor readings

Compiling the program shown in Figure 3 and running it on a supported computer will print a pair of numbers representing the "left" and "right" sensor readings. The numbers will update as the ambient light changes. To experiment, cover one of the speaker grills with your hand, perhaps followed by covering the other grill, and then move your hands away—the values should keep changing accordingly.

$ gcc -o lmutracker lmutracker.c -framework IOKit -framework CoreFoundation $ ./lmutracker 804 799 ^c $

Now that we know how to invoke one of the functions listed in Figure 2, we can adapt Figure 3's program to call the other functions. Let us look at excerpts of the code required.

To retrieve the current LED brightness value of the backlit keyboard, the relevant code would look something like Figure 4.

... kern_return_t kr; IOItemCount scalarInputCount = 1; IOItemCount scalarOutputCount = 1; SInt32 in_unknown = 0, out_brightness; kr = IOConnectMethodScalarIScalarO(dataPort, kGetLEDBrightnessID, scalarInputCount, scalarOutputCount, in_unknown, &out_brightness); // out_brightness is the brightness value; you might want to calibrate ...

Figure 4. Retrieving LED brightness of the backlit keyboard

Along similar lines, we can set the LED brightness of the backlit keyboard to a specific value using a program whose relevant excerpt is shown in Figure 5. You can empirically obtain a range of values that cause perceptible differences in brightness (say, from a minimum of zero to a maximum of 0xfff).

... kern_return_t kr; IOItemCount scalarInputCount = 2; IOItemCount scalarOutputCount = 1; SInt32 in_unknown = 0, in_brightness, out_brightness; in_brightness = 0xf00; // some particular value kr = IOConnectMethodScalarIScalarO(dataPort, kSetLEDBrightnessID, scalarInputCount, scalarOutputCount, in_unknown, in_brightness, &out_brightness); ...

Figure 5. Setting LED brightness of the backlit keyboard

Figure 6 shows a code excerpt for fading the LED brightness of the backlit keyboard from its current value to a target value over a specified duration.

... kern_return_t kr; IOItemCount scalarInputCount = 3; IOItemCount scalarOutputCount = 1; SInt32 in_unknown = 0, in_brightness, in_time_ms, out_brightness; in_brightness = 0xf00; // some particular target value in_time_ms = 15000; // time in ms kr = IOConnectMethodScalarIScalarO(dataPort, kSetLEDFadeID, scalarInputCount, scalarOutputCount, in_unknown, in_brightness, in_time_ms, &out_brightness); ...

Figure 6. Fading the LED brightness of the backlit keyboard

Note that Figures 4, 5, and 6 show an input argument called in_unknown—I have not attempted to determine the purpose of this argument. If I do, I will update this page.

Finally, since we are on the topic of light and brightness, let us complete the picture by looking at how to programmatically set a notebook display's brightness. The latter can be manipulated as a floating-point parameter whose value can lie between 0.0 and 1.0. This value can be retrieved and set through the IODisplayGetFloatParameter() and IODisplaySetFloatParameter() functions, which are available in the I/O Kit framework. Figure 7 shows a program that retrieves the current brightness of the main display and optionally (if an argument is given) sets it to a given brightness value.

// display-brightness.c #include <stdio.h> #include <IOKit/graphics/IOGraphicsLib.h> #include <ApplicationServices/ApplicationServices.h> #define PROGNAME "display-brightness" void usage(void) { fprintf(stderr, "usage: %s <brightness>\n" " where 0.0 <= brightness <= 1.0\n", PROGNAME); exit(1); } int main(int argc, char **argv) { CGDisplayErr dErr; io_service_t service; CGDirectDisplayID targetDisplay; CFStringRef key = CFSTR(kIODisplayBrightnessKey); float brightness = HUGE_VALF; switch (argc) { case 1: break; case 2: brightness = strtof(argv[1], NULL); break; default: usage(); break; } targetDisplay = CGMainDisplayID(); service = CGDisplayIOServicePort(targetDisplay); if (brightness != HUGE_VALF) { // set the brightness, if requested dErr = IODisplaySetFloatParameter(service, kNilOptions, key, brightness); } // now get the brightness dErr = IODisplayGetFloatParameter(service, kNilOptions, key, &brightness); if (dErr != kIOReturnSuccess) { fprintf(stderr, "operation failed\n"); } else { printf("brightness of main display is %f\n", brightness); } exit(dErr); }

Figure 7. Retrieving and setting a display's brightness

To compile the program in Figure 7, we will need to link with IOKit.framework and ApplicationServices.framework.

$ gcc -o display-brightness display-brightness.c \ -framework IOKit -framework ApplicationServices $ ./display-brightness brightness of main display is 0.162500 $ ./display-brightness 1.0 brightness of main display is 1.000000

What Next?

I must admit that the motion sensor experiments generated far more interest than I had expected. People found an incredible variety of "using" the motion sensor readings. I hope the information in this discussion will also help people give vent to their creative fervor.

Amit Singh