Controlling LEGO Wedo Motor with Python GUI App in Linux
LEGO Wedo 2.0 is a fantastic starting point for learning robotics. I developed a simple GUI application to control the LEGO motor using Python, and in this article, I’ll share my journey of selecting the development environment and building the Python app.
Learning Resources
During my initial research, I came across two helpful articles: Controlling a WeDo 2.0 motor and WeDo 2.0 – reverse engineering. Both articles highlight pygattlib, a Python library for interfacing with BLE (Bluetooth Low Energy) devices on Linux. The LEGO Wedo 2.0 Smart Hub is a BLE device, making this library essential for the project.
Command Line Tools and Development Environment
Before diving into coding, you can use command-line tools like hcitool and gatttool to scan for Bluetooth devices and test connections.
-
Scan for BLE Devices:
sudo hcitool -i hci0 lescan LE Scan ... 98:07:2D:DD:98:56 (unknown) 98:07:2D:DD:98:56 LPF2 Smart Hub
-
Connect to the Device:
gatttool -I [ ][LE]> connect <ble address>
Can I Use Windows Subsystem for Linux (WSL)?
No. You will get the following error message:
Invalid device: Address family not supported by protocol
Can I use Linux in VMWare Workstation for development?
No, BLE support is not available in Linux virtual machines on VMware Workstation, as confirmed by this this StackOverflow answer
Building a Python GUI App to Control LEGO Wedo Motor on Raspberry Pi
Ultimately, I chose to develop the Python app on a Raspberry Pi, as I didn’t have access to a dedicated Linux PC.
Installing Dependencies
Install the necessary libraries and gattlib:
sudo apt-get update
sudo apt-get install libbluetooth-dev bluez bluez-hcidump libboost-python-dev libboost-thread-dev libglib2.0-dev
sudo pip install gattlib
Creating the GUI Application
The GUI app features a simple window with a label and buttons for motor control:
import Tkinter as tk
from gattlib import DiscoveryService
from gattlib import GATTRequester
from time import sleep
def run():
global button_run
button_run.after(DELAY, motor_run)
def stop():
global button_stop
button_stop.after(DELAY, motor_stop)
def connect():
global button_disconnect
button_disconnect.after(DELAY, smart_hub_connect)
def disconnect():
global button_disconnect
button_disconnect.after(DELAY, smart_hub_disconnect)
def up():
global button_up
button_up.after(DELAY, motor_up)
def down():
global button_down
button_down.after(DELAY, motor_down)
root = tk.Tk()
root.title("Lego Wedo 2.0 Motor Control")
label = tk.Label(root, fg="dark green", text='N/A')
label.pack()
button_connect = tk.Button(root, text='Connect Smart Hub', width=BUTTON_WIDTH, command=connect)
button_connect.pack()
button_disconnect = tk.Button(root, text='Disconnect Smart Hub', width=BUTTON_WIDTH, command=disconnect, state='disabled')
button_disconnect.pack()
button_run = tk.Button(root, text='Run motor', width=BUTTON_WIDTH, command=run, state='disabled')
button_run.pack()
button_up = tk.Button(root, text='Speed up', width=BUTTON_WIDTH, command=up, state='disabled')
button_up.pack()
button_down = tk.Button(root, text='Speed down', width=BUTTON_WIDTH, command=down, state='disabled')
button_down.pack()
button_stop = tk.Button(root, text='Stop motor', width=BUTTON_WIDTH, command=stop, state='disabled')
button_stop.pack()
root.mainloop()
Connecting to the LEGO Smart Hub
When you click the connect button, the app uses DiscoveryService
to list available devices and GATTRequester
to connect to the Smart Hub:
def smart_hub_connect():
service = DiscoveryService("hci0")
devices = service.discover(2)
for address, name in devices.items():
if name != '' and 'Smart Hub' in name:
label['text'] = address
global button_run, button_stop, button_disconnect, req
button_connect['state'] = 'disabled'
button_run['state'] = 'normal'
button_stop['state'] = 'normal'
button_disconnect['state'] = 'normal'
button_up['state'] = 'normal'
button_down['state'] = 'normal'
req = GATTRequester(address, True, "hci0")
break
Controlling the Motor
To run the motor, send a 4-byte sequence to the specific handler:
def motor_run():
global req
if req != None:
req.write_by_handle(0x3d, str(bytearray([0x01, 0x01, 0x01, 0x64]))
)
The first byte represents the motor port (01 or 02).
Use gatttool to test port changes and see how they affect the motor:
[98:07:2D:DD:98:56][LE]> char-read-hnd 0015
Characteristic value/descriptor: 01 01 00 01 01 00 00 00 01 00 00 00
[98:07:2D:DD:98:56][LE]> char-read-hnd 0015
Characteristic value/descriptor: 02 01 01 01 01 00 00 00 01 00 00 00
Speed control logic:
MAX_SPEED = 100
MIN_SPEED = 1
SPEED_CHANGE = 4
current_speed = 100
req = None
def motor_up():
global req, current_speed
if req != None:
if current_speed == MAX_SPEED:
return
current_speed += SPEED_CHANGE
req.write_by_handle(HANDLE, str(bytearray([0x01, 0x01, 0x01, current_speed])))
sleep(WEDO_DELAY)
def motor_down():
global req, current_speed
if req != None:
if current_speed == MIN_SPEED:
return
current_speed -= SPEED_CHANGE
req.write_by_handle(HANDLE, str(bytearray([0x01, 0x01, 0x01, current_speed])))
sleep(WEDO_DELAY)
Running the App with Proper Permissions
Running the app without root privileges will result in errors:
Instead of using sudo
, launch the app with gksudo:
gksudo python app.py
Tip for Windows Users: You can use SmarTTY to display the GUI of the remote Linux app.