LEDs and Battery Monitoring

New in version 2.2 is the ability to control the LEDs, and read the battery level, of some controllers. In this initial release the library supports LEDs on the PS3 and PS4 controllers, and battery monitoring for the PS3, PS4 and XB1 with the last of those only reporting battery levels when connected over bluetooth.

Enabling access to LEDs

Note

You must do the steps below if you want to write to LED state on your controller. If you do not, it will not work and you’ll probably encounter file permission errors.

We write to files in /sys/class/leds to change LED state on a connected controller. By default under linux these file nodes are not writeable for non-root users. To fix this, we need to add some udev rules, create a new group for users who should be able to access LEDs, and then add the appropriate user to that group.

Firstly create a new file, 90-led-permission.rules in /etc/udev/rules.d/ with the following contents:

1
2
3
4
5
6
# Assign any new nodes in the 'leds' subsystem (nodes in /sys/class/leds) to the group 'leds' to write without root

# Create a new group 'leds' before this will do anything!

SUBSYSTEM=="leds", ACTION=="add", RUN+="/bin/chgrp -R leds /sys%p", RUN+="/bin/chmod -R g=u /sys%p"
SUBSYSTEM=="leds", ACTION=="change", ENV{TRIGGER}!="none", RUN+="/bin/chgrp -R leds /sys%p", RUN+="/bin/chmod -R g=u /sys%p"

Then create a new leds group:

> sudo groupadd leds

Then add membership of that new group to the appropriate user. In this case we’ll use the user pi, the default user on the Raspberry Pi:

> sudo usermod -a -G leds pi

Once you’ve done all the above, you need to either restart the pi (simplest option) or, if you know what you’re doing, force a re-load of the udev rules and log out then back in again to pick up the new group membership. Once this is done, any new nodes corresponding to LEDs will be writeable for users in the leds group, and the user you selected will be a member of that group.

Writing to LEDs

There is no single standard API used to write to LEDs, because each controller is different. If your controller supports this, and in version 2.2 this is restricted to the DS3 and DS4 controllers, it will be described in the documentation for the controller class itself. Specifically, for the DS3 controller you can control each individual red LED to be on or off with approxeng.input.dualshock3.DualShock3.set_led(), and on the DS4 controller you can set the hue, saturation and value (brightness) of the front bar RGB LED with approxeng.input.dualshock4.DualShock4.set_leds() method. Because these methods are entirely controller specific, you should either user a form of the binder that requires that controller type, or explicitly check that you’ve got the right kind of controller (or handle the absence of these methods in your own code when you call them).

The examples below show how to use the LEDs on the DS3 (scanning across the four LEDs), and the light bar on the DS4 (animating a rainbow of colours):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from time import sleep

from approxeng.input.dualshock3 import DualShock3
from approxeng.input.selectbinder import ControllerResource, ControllerRequirement

while True:
    active_led = 0
    try:
        # Force waiting for a DS3
        with ControllerResource(ControllerRequirement(require_class=DualShock3)) as controller:
            while controller.connected:
                active_led = (active_led + 1) % 4
                for led_number in range(4):
                    if led_number == active_led:
                        controller.set_led(led_number + 1, 1)
                    else:
                        controller.set_led(led_number + 1, 0)
                sleep(0.2)
    except IOError:
        # No DS3 controller found, wait for a bit and try again
        print('Waiting for a DS3 controller connection')
        sleep(1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from time import sleep

from approxeng.input.dualshock4 import DualShock4
from approxeng.input.selectbinder import ControllerResource, ControllerRequirement

while True:
    hue = 0.0
    try:
        # Force waiting for a DS4 controller, as that's the only one with the call
        # to set the light bar in this way.
        with ControllerResource(ControllerRequirement(require_class=DualShock4)) as ds4:
            while ds4.connected:
                # Set the hue of the light bar, saturation and value default to 1.0
                ds4.set_leds(hue=hue)
                # Pause for a bit, advance hue, and go around again
                sleep(0.02)
                hue = hue + 0.01
                if hue > 1.0:
                    hue = 0.0
    except IOError:
        # No DS4 controller found, wait for a bit and try again
        print('Waiting for a DS4 controller connection')
        sleep(1)

Reading Battery Levels

Controllers now all have a property battery_level. Controllers that support battery monitoring (currently the DS3, DS4 and XB1 controller when connected wirelessly) will return a floating point value between 0.0 (empty) and 1.0 (full) to indicate the battery level of the associated controller. Controllers that do not support this feature will return None, so you potentially need to handle the return value not being a number.

Not all controllers report fine-grained battery levels. For example, the DS3 only reports four partial battery levels, so you’ll probably only see 0, 0.25, 0.5, 0.75, and 1.0, whereas the DS4 reports to the nearest 0.1 and the XB1 is slightly finer grained.

Note

Be aware that every time you read the battery_level property, the code has to open a file and read the value from it. While this is fast, because the file is a virtual one, the battery level only changes quite slowly so you should only query this property fairly infrequently to avoid excessive IO access. Checking every minute or so is almost certainly going to be good enough.

The updated show_controls.py script now indicates battery level if the controller supports it (see line 67 below):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import curses
import pprint
from time import sleep

from approxeng.input.selectbinder import ControllerResource


def main(screen):
    curses.curs_set(False)
    curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
    curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
    curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK)
    curses.init_pair(4, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
    curses.start_color()
    last_presses = None

    def red(s):
        screen.addstr(s, curses.color_pair(1))

    def green(s):
        screen.addstr(s, curses.color_pair(2))

    def yellow(s):
        screen.addstr(s, curses.color_pair(3))

    def magenta(s):
        screen.addstr(s, curses.color_pair(4))

    # Loop forever
    while True:
        try:
            with ControllerResource() as joystick:
                while joystick.connected:
                    # Check for presses since the last time we checked
                    joystick.check_presses()

                    screen.clear()

                    if joystick.has_presses:
                        last_presses = joystick.presses

                    # Print most recent presses set
                    screen.addstr(0, 0, 'last presses:')
                    if last_presses is not None:
                        for button_name in last_presses:
                            green(' {}'.format(button_name))

                    # Print axis values
                    screen.addstr(1, 0, 'axes:')
                    for axis_name in joystick.axes.names:
                        screen.addstr(' {}='.format(axis_name))
                        axis_value = joystick[axis_name]
                        if not isinstance(axis_value, tuple):
                            text = '{:.2f}'.format(axis_value)
                            if axis_value > 0:
                                green(text)
                            elif axis_value < 0:
                                red(text)
                            else:
                                yellow(text)
                        else:
                            x, y = axis_value
                            text = f'({x:.2f},{y:.2f})'
                            magenta(text)

                    # Print button hold times
                    screen.addstr(2, 0, 'hold times:')
                    for button_name in joystick.buttons.names:
                        hold_time = joystick[button_name]
                        if hold_time is not None:
                            screen.addstr(' {}='.format(button_name))
                            green('{:.1f}'.format(hold_time))

                    # Print some details of the controller
                    screen.addstr(3, 0, 'controller class: {}'.format(type(joystick).__name__))
                    battery_level = joystick.battery_level
                    if battery_level:
                        screen.addstr(4, 0, 'battery_level: {:.2f}'.format(joystick.battery_level))
                    screen.addstr(6, 0, pprint.pformat(joystick.controls, indent=2))

                    screen.refresh()
                    sleep(0.05)
        except IOError:
            screen.clear()
            screen.addstr(0, 0, 'Waiting for controller')
            screen.refresh()
            sleep(1.0)


curses.wrapper(main)