We are constantly trying out new technologies and better ways to support our students. One of the recent investments is a Lego EV3 Education set. However we do not build robots, we program them and take ’em to the NXT level. Pun intended. We also bought a dozen Raspberry Pi’s to dive into Linux. So we thought; Why not garnish our Pi with Lego Minstorms EV3. And since Pi speaks Python the idea came to life.
PrerequisitesWhen you plan to program Python on a Raspberry Pi, you need a Pi running your favorite flavour Linux. But that isn’t what this is about. You’ll no doubt find lots of web links that guide you trough the Raspberry Pi setup process. We are running Raspbian.
You will also need the Lego Mindstorms EV3 Brick Bluetooth protocol description. So, not unlike every project this one starts with searching the internet. Google came up with very few hits, not surprisingly, Lego Mindstorms EV3 is quite a new product. However this one showed to be a very good starting point.
Lego Minstorms EV3 software offers the user three kinds of Bluetooth messages;
- Logical
- Text
- Numerical
They share the same framework however the way the payload is coded in the message differs. We had a great head start at “Sioux .NET on Track” (see link above) so transmitting and receiving text was like taking candy from a baby…
Receiving EV3 BT MessagesReceiving LogicalWe wrote an EV3 program that transmits the logical value “true” whenever the sensor on port 1 is pressed.
Next we wrote a Python program that reads the Bluetooth interface. BTW, we are using a of the shelf Bluetooth dongle we bought at our local computer shop. Installing it on our Pi’s took no more time then the time needed to type:
sudo apt-get update
sudo apt-get install bluetooth bluez-utils blueman
Ok, granted, paring devices from the command line is kind a pain in the … But, when paring is initiated from within the GUI it should not be too difficult.The Python code we started with is rather basic, and should therefore be easily understood.
#! /usr/bin/env python
import serial
import time
EV3 = serial.Serial('/dev/rfcomm0')
print "Listening for EV3 Bluetooth messages, press CTRL C to quit."
try:
while 1:
n = EV3.inWaiting()
if n <> 0:
s = EV3.read(n)
for n in s:
print "x%02X" % ord(n),
print
else:
# No data is ready to be processed
time.sleep(0.5)
except KeyboardInterrupt:
pass
EV3.close()
When this script is executed the hex code of every received byte is output to the terminal window, thus allowing further analysis.
pi@RPi-KK ~/python_projects $ ./receiver.py
Listening for EV3 Bluetooth messages, press CTRL C to quit.
x0F x00 x01 x00 x81 x9E x07 x53 x74 x61 x74 x75 x73 x00 x01 x00
x01 'True'
x0F x00 x01 x00 x81 x9E x07 x53 x74 x61 x74 x75 x73 x00 x01 x00
x01 'True'
x0F x00 x01 x00 x81 x9E x07 x53 x74 x61 x74 x75 x73 x00 x01 x00
x01 'True'
x0F x00 x01 x00 x81 x9E x07 x53 x74 x61 x74 x75 x73 x00 x01 x00
x01 'True'
x0F x00 x01 x00 x81 x9E x07 x53 x74 x61 x74 x75 x73 x00 x01 x00
x01 'True'
AnalysisThe first two bytes contain numerical information, little endian. They read 0x000F or 15. That is the exact amount of bytes in the remainder of the message. The next two bytes contain a message counter, however it isn’t incremented with every new message the EV3 brick sends.The next two bytes are referred to as ID bytes. They do not change and read 0x81 and 0x9E respectively. Next is one byte depicting the number of bytes in the mailbox name, 7 in this case. That is 6 bytes for “Status” and one trailing NULL character.The third from last are two bytes, little endian, depicting the number of bytes in the payload. When transmitting boolean values the payload consists of only one byte. The LSBit is set when transmitting “true”, when “false” is send that byte reads NULL.
Receiving TextWe proceeded with the same strategy for analyzing text messages.
pi@RPi-KK ~/python_projects $ ./receiver.py
Listening for EV3 Bluetooth messages, press CTRL C to quit.
x15 x00 x01 x00 x81 x9E x07 x53 x74 x61 x74 x75 x73 x00 x07 x00
x49 x27 x6D x20 x4F x6B x00 "I'm Ok\0"
x15 x00 x01 x00 x81 x9E x07 x53 x74 x61 x74 x75 x73 x00 x07 x00
x49 x27 x6D x20 x4F x6B x00 "I'm Ok\0"
AnalysisThe structure of the entire message is virtually the same … Except, the number of bytes in the payload now is 0x0007, exactly the amount of bytes needed for “I’m Ok” and the trailing NULL character.
Receiving NumericalAgain, virtually the same setup returned:
Listening for EV3 Bluetooth messages, press CTRL C to quit.
x12 x00 x01 x00 x81 x9E x07 x53 x74 x61 x74 x75 x73 x00 x04 x00
x00 x00 x80 x3F "1.0" (LSB First)
x12 x00 x01 x00 x81 x9E x07 x53 x74 x61 x74 x75 x73 x00 x04 x00
x00 x00 x00 x40 "2.0"
x12 x00 x01 x00 x81 x9E x07 x53 x74 x61 x74 x75 x73 x00 x04 x00
x00 x00 x40 x40 "3.0"
x12 x00 x01 x00 x81 x9E x07 x53 x74 x61 x74 x75 x73 x00 x04 x00
x00 x00 x80 x40 "4.0"
AnalysisThe payload byte count shows that numerics are transmitted in four bytes, however the counter variable that is transmitted and incremented every cycle does not show up very transparently … That’s when we thought Lego outsmarted us. But not really, after a visit to IEEE 754 Floating point converter we discovered the data was merely send in IEEE 754 floating point format.
A Smart(er) ReceiverThe code above might do great for analysis, the message received however is not very “readable”, nor is it robust in terms synchronization with the transmitter. Therefore we’ve written a new receiver.
#! /usr/bin/env python
import serial
import time
import struct
EV3 = serial.Serial('/dev/rfcomm0')
print "Listening on rfcomm0 for EV3 Bluetooth messages, press CTRL C to quit."
try:
while 1:
n = EV3.inWaiting()
if n >= 2: # Get the number of bytes in this message
s = EV3.read(2)
# struct.unpack returns a tuple unpack using []
[numberOfBytes] = s
truct.unpack("<H", s)
print numberOfBytes,
# Wait for the message to complete
while EV3.inWaiting() < numberOfBytes:
time.sleep(0.01)
s = s + EV3.read(numberOfBytes)
[messageCounter] = struct.unpack("<H", s[2:4])
print messageCounter,
ID1 = ord(s[4])
ID2 = ord(s[5])
print ID1, ID2,
s = s[6:]
# Get the mailboxNameLength and the mailboxName
mailboxNameLength = ord(s[0])
mailboxName = s[1:1+mailboxNameLength-1]
print mailboxNameLength, mailboxName,
s = s[mailboxNameLength+1: ]
# Get the payloadLength and the payload
[payloadLength] = struct.unpack("<H", s[0:2])
payload = s[2:2+payloadLength]
print payloadLength, payload
else:
# No data is ready to be processed yield control to system
time.sleep(0.01)
except KeyboardInterrupt:
print "Bye"
pass
EV3.close()
This software only reads two bytes from the RFCOMM device and determines from them the number of bytes to retrieve in the message. Then waits for the entire message to complete. This is by the way a great example of how to use the python struct. It allows to treat the data that was received in plain ascii code as integers, shorts or even float data.
What's Next?In a next post we 'll be looking at transmitting messages from Raspberry Pi to EV3.
Comments