Controlling LEGO Wedo Motor with Python GUI App in Linux
LEGO Wedo 2.0 is a good start for learning robotics. I created a simple GUI app controlling the LEGO motor using Python. In this article, I will share my experience in how to select the development environment and how to build the Python app.
When I searched for `LEGO Wedo Python’ in Google, I found two informative articles: Controlling a WeDo 2.0 motor and WeDo 2.0 – reverse engineering. Both of them mentioned pygattlib, which is a Python library (Linux only) to use the GATT Protocol for BLE (Bluetooth Low Energy) devices. The smart hub included in LEGO Wedo 2.0 is a BLE device.
Command Line Tools and The Development Environment
Before writing code, we can use hcitool and gatttool to scan the Bluetooth device and test the connection.
Get the BLE address and device name:
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) for development?
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. You will fail to run hcitool in VMWare virtual machine.
According to the answer from StackOverflow, VMWare does not support BLE in Linux guests.
Building a Simple Python GUI App to Control LEGO Wedo Motor on Raspberry Pi
I finally decided to build the Python app on Raspberry Pi because I don’t have a Linux PC.
Install dependent 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
Create the UI Window that includes a label and some buttons:
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()
When press the connect button, use DiscoveryService to get all available devices and use GATTRequester to connect the device. My device name contains `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
To run the motor, we just need to send a sequence of 4 bytes to a 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 port. The value is 01 or 02.
Change the hardware port to see what will happen via gatttool:
[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
The last byte defines the motor speed. Here is the code for adjusting the speed:
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)
You will fail to run the app if you don’t have a root privilege:
Should you run the app with
sudo'? No, you have to run the app with gksudo’:
gksudo python app.py
In Windows, we can use SmarTTY to display GUI for the remote Linux app.