08
- April
2023
Posted By : Dave Hartburn
Lego Mindstorms EV3 and remote control

The Lego Mindstorms EV3 is the programmable ‘brick’ at the heart of the Lego Mindstorms system. However one thing lacking in Mindstorms is remote control options. There is an infrared controller, but this has a limited range of about 2 meters. You can associate a PS4 game controller, but options for an Xbox controller seem to be limited. Using the menus, we could not get one to pair. It needed some further investigation and it was clear the power of Python (rather than the default graphical language) was going to be needed. Rather pleasingly, when you run Python, it is actually running a small Linux distribution, giving you a great deal of power and control of the EV3 unit.

With a SD card to provide an image, you can install python by following the instructions at Lego Education. This will run the Pybricks distribution of MicroPython, a limited version of python designed for microcontrollers. Because of this it does not have all the modules available that you may expect from a full blown version of Python.

To see what we have available, boot the EV3 into the Python image (EV3DEV), start a new blank project and add the line help("modules"). You should see a list of modules similar to below:

__main__          mmap              pybricks/tools    ufcntl
_thread           nxtdevices_c      pybricks/uev3dev/__init__           uhashlib
array             parameters_c      pybricks/uev3dev/_alsa              uheapq
bluetooth_c       pybricks/__init__ pybricks/uev3dev/_wand              uio
btree             pybricks/bluetooth                  pybricks/uev3dev/display            ujson
builtins          pybricks/display  pybricks/uev3dev/i2c                umachine
cmath             pybricks/ev3brick pybricks/uev3dev/messaging          uos
core              pybricks/ev3devices                 pybricks/uev3dev/sound              urandom
ev3devices_c      pybricks/ev3devio pybricks/uev3dev/util               ure
experimental_c    pybricks/experimental               robotics_c        uselect
ffi               pybricks/hubs     sys               usignal
framebuf          pybricks/iodevices                  termios           usocket
gc                pybricks/media/ev3dev               tools             ussl
hubs_c            pybricks/messaging                  ubinascii         ustruct
iodevices_c       pybricks/nxtdevices                 ucollections      utime
math              pybricks/parameters                 ucryptolib        utimeq
media_ev3dev_c    pybricks/robotics uctypes           uwebsocket
micropython       pybricks/speaker  uerrno            uzlib

Using a wireless keyboard

A wireless keyboard is probably the easiest remote input, as wireless keyboards are very common and follow a well defined input standard. However there is no native support within Pybricks. You can read from stdin, but if you don’t have a remote console, that is not going to work. But, a wireless keyboard will appear like a standard Linux input device. From Visual Studio Code, open an SSH session to the device and then (once you have plugged the wireless keyboard USB dongle into the USB port), type lsusb. If it is listed then keyboard input can be tested with evtest. Select the input device that looks like your keyboard (usually event2) and tap a few keys. If you see output, then it can receive input from the keyboard.

All very well, but how can this be used from python? Hard coding in the device event number, the following code will show keyboard presses in terms of a code number.

#!/usr/bin/env pybricks-micropython
from pybricks.hubs import EV3Brick
from pybricks.ev3devices import (Motor, TouchSensor, ColorSensor,
                                 InfraredSensor, UltrasonicSensor, GyroSensor)
from pybricks.parameters import Port, Stop, Direction, Button, Color
from pybricks.tools import wait, StopWatch, DataLog
from pybricks.robotics import DriveBase
from pybricks.media.ev3dev import SoundFile, ImageFile

import struct
import time
import sys

# Reading raw keyboard input from /dev/input, see:
# https://stackoverflow.com/questions/5060710/format-of-dev-input-event


# Create your objects here.
ev3 = EV3Brick()

device="/dev/input/event2"

"""
FORMAT represents the format used by linux kernel input event struct
See https://github.com/torvalds/linux/blob/v5.5-rc5/include/uapi/linux/input.h#L28
Stands for: long int, long int, unsigned short, unsigned short, unsigned int
"""
FORMAT = 'llHHI'
EVENT_SIZE = struct.calcsize(FORMAT)

#open file in binary mode
in_file = open(device, "rb")

event = in_file.read(EVENT_SIZE)

print("Starting keyboard test....")
while event:
    (tv_sec, tv_usec, type, code, value) = struct.unpack(FORMAT, event)

    if type != 0 or code != 0 or value != 0:
        print("Event type %u, code %u, value %u at %d.%d" % \
            (type, code, value, tv_sec, tv_usec))
    else:
        # Events with code, type and value == 0 are "separator" events
        print("===========================================")

    event = in_file.read(EVENT_SIZE)

in_file.close()

That is not immediately useful. What key is being pressed? How does the code relate to something useful like ‘A’? All we receive is the raw code number for each key.

There is a lookup table in the linux headers, /usr/include/linux/input-event-codes.h, on most linux systems, but not on pybricks. If you have access to a linux system, a file of ‘<key> <code>’ can be created with grep '#define' /usr/include/linux/input-event-codes.h | grep 'KEY_' | awk '{printf("%s %s\n", $2, $3)}' > keyMap, or my copy can be downloaded from the github page.

In addition we don’t need to print all the event output. A keyboard event is type=1, the key pressed will be returned in the code. A key up event has a value of 0 and a key down has a value of 1. If we only want to look at key down events, we can simply use an if statement to check for type=1, value=1.

To make things a little more interesting, if a B is pressed, it makes the EV3 beep. This can now become a framework for a keyboard controlled program, triggering motors or servos.

#!/usr/bin/env pybricks-micropython
from pybricks.hubs import EV3Brick
from pybricks.ev3devices import (Motor, TouchSensor, ColorSensor,
                                 InfraredSensor, UltrasonicSensor, GyroSensor)
from pybricks.parameters import Port, Stop, Direction, Button, Color
from pybricks.tools import wait, StopWatch, DataLog
from pybricks.robotics import DriveBase
from pybricks.media.ev3dev import SoundFile, ImageFile

import struct
import time
import sys

# Reading raw keyboard input from /dev/input, see:
# https://stackoverflow.com/questions/5060710/format-of-dev-input-event


# Create your objects here.
ev3 = EV3Brick()

device="/dev/input/event2"

# Read keyboard event codes from keyMap
keysIn=open("keyMap", "r")
kbdCodes={}
for line in keysIn:
    # Split the line on whitespace
    sp=line.split()
    
    # Ensure there are at least 2 things on the line
    if(len(sp)>=2):
        if(sp[0].startswith("KEY_")):
            key=sp[0]
            # The code may not be valid hex, ignore
            try:
                code=int(sp[1],0)
                #print("Keycode {} to {}".format(code, key))
                kbdCodes[code]=key
            except:
                pass


"""
FORMAT represents the format used by linux kernel input event struct
See https://github.com/torvalds/linux/blob/v5.5-rc5/include/uapi/linux/input.h#L28
Stands for: long int, long int, unsigned short, unsigned short, unsigned int
"""
FORMAT = 'llHHI'
EVENT_SIZE = struct.calcsize(FORMAT)

#open file in binary mode
in_file = open(device, "rb")

event = in_file.read(EVENT_SIZE)

print("Starting keyboard test....")

while event:
    (tv_sec, tv_usec, type, code, value) = struct.unpack(FORMAT, event)

    if type==1 and value==1:
        # We should have a keycode mapping for this, but use try, just in case
        try:
            key=kbdCodes[code]
        except:
            key="NONE"
        print(key,"pressed")

    # Lets try something with the brick, beep on a B
    if(key=="KEY_B"):
        ev3.speaker.beep()

    event = in_file.read(EVENT_SIZE)

in_file.close()

Controlling with an Xbox controller

Although documentation is limited, you can pair an Xbox controller to a EV3, but need to add a wireless card and pull in an extra package first. Eventually I discovered this excellent guide. Add a wireless card, open a SSH session and:

sudo apt update
sudo apt upgrade           # This may take a while
sudo apt install sysfsutils
sudo sh -c 'echo module/bluetooth/parameters/disable_ertm = 1 >> /etc/sysfs.conf'
sudo service sysfsutils restart 

bluetoothctl
[bluetooth]# power off
[bluetooth]# power on
[bluetooth]# scan on
[bluetooth]# trust <mac>
[bluetooth]# pair <mac>
[bluetooth]# connect <mac>

Once done, the Xbox controller will typically appear as /dev/input/event2 (assuming you have unplugged the wireless keyboard). It can be tested with evtest in the same way as above. Like the keyboard, you can read and process the raw events in the same way. Hugbug has provided a similar program frame work at https://github.com/hugbug/ev3/blob/master/xbox-info/xbox-info.py.

Network control

With a wireless network card, you could use the sockets library, provide a socket to connect to and send commands to the EV3. I will expand on this later, but a rough guide, see the focus client and server on my Astroberry repository.

Leave a Reply