WeDo 2.0 – reverse engineering

Este artigo é a parte 1 de 6 da série  WeDo 2.0 - reverse engineering

This post tries to gather all the information I collected in the last weeks related to the WeDo 2.0.

I’m not a programmer but I’m a very stubborn guy so after I managed to get my linux systems (my Ubuntu laptop, some Raspberry Pi’s and my two Mindstorms EV3) controlling the SBrick I told to myself: I’m gonna make the same with this new WeDo 2.0 system no matter what it takes.

So first things first: let’s use my Android mobile to inspect the WeDo 2.0 Hub. Nordic has a very good (and free) app that I like to use: nRF Master Control Panel (recently renamed to nRF Connect). After connecting to the Hub we find 6 services:

BLE services

Some of this services, like “Battery Service” are known BLE services so [hopefully] it will be easy to use.

There’s also a very important finding at the top:

“U0001 A0:E6:F8:1E:58:57

“A0:E6:F8:1E:58:57” is the Bluetooth address of my Hub (similar to the MAC Address of every network device). It will be used *a lot* in the rest of this post.

And “U0001” is the friendly name that the Hub advertises – that’s the name that shows up in the LEGO Education WeDo 2.0 App when connecting to the Hub:

WeDo 2.0 making a connection

A note about this name: before I finally managed to make the LEGO Education WeDo 2.0 App work in my Android phone, my Hub advertised itself as “LPF2 Smart Hub 2 I/O”. So the LEGO App changed it to “u0001”, probably at the first time it connected to it (but since my Hub was first used by 3 other AFOL’s at Paredes de Coura I’m not sure if the process is automatic or the user is given some kind of option).

So the default (factory set) name of the Hub is “LPF2 Smart Hub 2 I/O” – LEGO Power Functions 2 Smart Hub 2 I/O”. Not much to speculate here: LEGO announced that Power Functions and Mindstorms will adopt a new plug type so new devices are expected, this is just the first one. But “Smart Hub 2 I/O” is interesting… does that means that there will be other Smart Hubs? Perhaps even a “Smart Hub 4 I/O”? That would answer some of the points I have been discussing with Fernando Conchas like “what’s the use for a 4.5 Volt device in LEGO Technic unless there’s also another device with better power features just waiting to come out”?

Now let’s look deeper to those BLE services…

I can use the nRF App and take a lot of screenshots but now that I know the BT address I will switch to my Ubuntu laptop and use 2 of the available BlueZ (the native Linux Bluetooth stack) functions, ‘hcitool’ and ‘gatttool’

$ sudo hcitool -i hci0 lescan
LE Scan ...
A0:E6:F8:1E:58:57 (unknown)
A0:E6:F8:1E:58:57 LPF2 Smart Hub 2 I/O
sudo gatttool -i hci0 -b A0:E6:F8:1E:58:57 --primary
attr handle = 0x0001, end grp handle = 0x0007 uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle = 0x0008, end grp handle = 0x000b uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle = 0x000c, end grp handle = 0x002f uuid: 00001523-1212-efde-1523-785feabcd123
attr handle = 0x0030, end grp handle = 0x003e uuid: 00004f0e-1212-efde-1523-785feabcd123
attr handle = 0x003f, end grp handle = 0x0045 uuid: 0000180a-0000-1000-8000-00805f9b34fb
attr handle = 0x0046, end grp handle = 0xffff uuid: 0000180f-0000-1000-8000-00805f9b34fb

The ‘gatttool’ command is a powerfull tool for BLE – in the past, without a proper BLE library for python, I used it (through python system calls) to talk with the SBrick. Clumsy but… hey, I said I’m not a programmer 😉

The ‘gatttool’ can run in an interactive mode that allows us to establish a connection and keep it until we disconnect instead of making a new connection each time we want to test a command:

$ sudo gatttool -i hci0 -I
[                 ][LE]> connect A0:E6:F8:1E:58:57
Attempting to connect to A0:E6:F8:1E:58:57
Connection successful
[A0:E6:F8:1E:58:57][LE]>

In this “interactive” session we just send ‘primary’ to get the same output as using the command with ‘–primary’ option [but sometimes the commands differ a bit, so use ‘help’ and ‘–help’ to know what to use.

So the ‘primary’ command gets a list of the primary services offered by the WeDo 2.0 Hub. Of course, those are the same 6 services found by the Nordic app but that screenshot looks much better as Nordic developers added lots of intelligence to it.

First service:

attr handle = 0x0001, end grp handle = 0x0007 uuid: 00001800-0000-1000-8000-00805f9b34fb

The Nordic app shows just:

Generic Access
UUID: 0x1800
PRIMARY SERVICE

So this service has 7 handles assigned (from 0x0001 to 0x0007) and serves a well know service (the ‘Generic Access‘) so it’s UUID is shortened to just 0x1800 instead of ‘00001800-0000-1000-8000-00805f9b34fb’.

Bluetooth specification for ‘Generic Access’ defines 5 properties:

  • Device Name (0x2A00)
  • Appearance (0x2A01)
  • Peripheral Privacy Flag (0x2A02)
  • Reconnection Address (0x2A03)
  • Peripheral Preferred Connection Parameters (0x2A04)

From these list, only ‘Device Name’ and ‘Appearance’ are defined as ‘Mandatory’.

With the gatttool we read these properties with command ‘char-read-uuid’:

[A0:E6:F8:1E:58:57][LE]> char-read-uuid 0x2A00
handle: 0x0003      value: 75 30 30 30 31 
[A0:E6:F8:1E:58:57][LE]> char-read-uuid 0x2A01
handle: 0x0005      value: 00 00 
[A0:E6:F8:1E:58:57][LE]> char-read-uuid 0x2A02
Error: Read characteristics by UUID failed: No attribute found within the given range
[A0:E6:F8:1E:58:57][LE]> char-read-uuid 0x2A03
Error: Read characteristics by UUID failed: No attribute found within the given range
[A0:E6:F8:1E:58:57][LE]> char-read-uuid 0x2A04
handle: 0x0007      value: 50 00 a0 00 00 00 e8 03

So ‘Peripheral Privacy Flag’ and ‘Reconnection Address’ were not implemented by LEGO. But what’s the meaning of this hexadecimal values?

Device Name‘ is a string so we just convert it to ASCII (we can use an online tool like RapidTables):

75 30 30 30 31 -> u0001

Ah-ha!

Appearance‘ is two-byte value “composed of a category (10-bits) and sub-categories (6-bits)” that classifies the device. Since it is ‘0’, it’s not classified in any known category (so it is ‘Unknown’).

Peripheral Preferred Connection Parameters‘ is an 8-byte value containing 4 parameters (each one is 2-byte):

Minimum Connection Interval
Maximum Connection Interval
Slave Latency
Connection Supervision Timeout Multiplier

Each value is written in reverse order so

Minimum Connection Interval =005h = 80d
Maximum Connection Interval = 000ah = 160
Slave Latency = 0
Connection Supervision Timeout Multiplier = 03e8h=1000d

According to definition, the Min/Max Connection Interval values should be multiplied by 1.25 so the range is in fact 100~200 [ms].

So let’s return to the Android and validate these:

WeDo 2.0 'Generic Access' primary service

Yes, it certainly looks good!

Next post we’ll see another well-know service: the ‘Battery Service’.

 

WeDo 2.0 Battery Service

Este artigo é a parte 2 de 6 da série  WeDo 2.0 - reverse engineering

In first post we have seen that the WeDo 2.0 Hub offers a “Battery Service” Primary Service:

[A0:E6:F8:1E:58:57][LE]> primary
...
attr handle: 0x0046, end grp handle: 0xffff uuid: 0000180f-0000-1000-8000-00805f9b34fb
...

‘180F’ is defined in the Bluetooth Standard as a ‘Battery Service‘. LEGO assigned all handles from 0x0046 to 0xFFFF to this service (not quite true: since there is no other Primary Service, gatttool assumes 0xFFFF as the last handle).

What handles and characteristics?

[A0:E6:F8:1E:58:57][LE]> characteristics 0x0046 0xffff
handle: 0x0047, char properties: 0x12, char value handle: 0x0048, uuid: 00002a19-0000-1000-8000-00805f9b34fb
[A0:E6:F8:1E:58:57][LE]> char-desc 0x0046 0xffff
handle: 0x0046, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0047, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0048, uuid: 00002a19-0000-1000-8000-00805f9b34fb
handle: 0x0049, uuid: 00002902-0000-1000-8000-00805f9b34fb

So there is  just one useful characterist, the one with UUID ‘00002a19-0000-1000-8000-00805f9b34fb’ or just ‘2a19’ assigned to handle 0x0048.

There are 3 other characteristics (‘2800’, ‘2803’ and ‘2902’) each one assigned to one handle (‘0x0046’, ‘0x0048’ and ‘0x49’). What’s their purpose?

The first characteristic (‘2800’) contains the UUID of the Primary Service (we already know that it is ‘180F’):

[A0:E6:F8:1E:58:57][LE]> char-read-hnd 0x046
Characteristic value/descriptor: 0f 18

The second characteristic (‘2803’) describes the Service – something we already know from the ‘characteristics’ command:

[A0:E6:F8:1E:58:57][LE]> char-read-hnd 0x047
Characteristic value/descriptor: 12 48 00 19 2a

The first byte is the ‘characteristic properties’. Each bit has a meaning:

Broadcast: 01h
Read: 02h
Write without response: 04h
Write: 08h
Notify:10h
Indicate: 20h
Authenticated Signed Writes:40h
Extended Properties: 80h

So ’12h’ means ‘notify’ + ‘read’ so we can read and the battery level and we can also ask for notifications.

The second and third bytes specify the handle associated to the battery level: ‘0048h’.

The fourth and fifth bytes specify the characteristic UUID of the battery level: ‘2a19’.

Now the last characteristic (‘2902’) is used for notification and indication control.

[A0:E6:F8:1E:58:57][LE]> char-read-hnd 0x049
Characteristic value/descriptor: 00 00

Since it’s value is ‘0’ it means ‘Notifications and indications disabled’

Now let’s read the battery level, at the third characteristic (UUID: ‘2a19’/ Handle: 0x0048)

[A0:E6:F8:1E:58:57][LE]> char-read-hnd 0x048
Characteristic value/descriptor: 64

64h = 100d so battery level is 100%

We can also use this other command:

[A0:E6:F8:1E:58:57][LE]> char-read-uuid 2a19
handle: 0x0048      value: 64

Now let’s check with Android nRF Connect App:

WeDo 2.0 Battery Service

Seems ok once again.

Until now we just accessed information defined in the Bluetooth standard. Maybe in the future the Bluetooth SIG decide to include motors, lights and other devices in the standard but for now each vendor (like LEGO) decides how to communicate with its gadgets and if it doesn’t release that information to the public it’s almost impossible to develop third-party applications for it.

LEGO promised to release a SDK in the first half of 2016. That’s a good decision, everyone (including LEGO) will benefit if everyone can create it’s own application and even integrate it with other tools or systems. But we are already in the second half of 2016 and no SDK…

Luckily there are already a few pieces of information scattered around the Web…

Next post: the WeDo 2.0 Hub button.

WeDo 2.0 LED Button Service

Este artigo é a parte 3 de 6 da série  WeDo 2.0 - reverse engineering

And we continue digging into the  WeDo 2.0 Hub primary services, now with the “Nordic LED Button Service”

WeDo 2.0 Hub BLE services

[A0:E6:F8:1E:58:57][LE]> primary
...
attr handle: 0x000c, end grp handle: 0x002f uuid: 00001523-1212-efde-1523-785feabcd123
...

So this Primary service uses handles from 0x000c to 0x002f. Let’s look deeper:

[A0:E6:F8:1E:58:57][LE]> characteristics 000c 002f
handle: 0x000d, char properties: 0x0a, char value handle: 0x000e, uuid: 00001524-1212-efde-1523-785feabcd123
handle: 0x0010, char properties: 0x12, char value handle: 0x0011, uuid: 00001526-1212-efde-1523-785feabcd123
handle: 0x0014, char properties: 0x10, char value handle: 0x0015, uuid: 00001527-1212-efde-1523-785feabcd123
handle: 0x0018, char properties: 0x12, char value handle: 0x0019, uuid: 00001528-1212-efde-1523-785feabcd123
handle: 0x001c, char properties: 0x12, char value handle: 0x001d, uuid: 00001529-1212-efde-1523-785feabcd123
handle: 0x0020, char properties: 0x12, char value handle: 0x0021, uuid: 0000152a-1212-efde-1523-785feabcd123
handle: 0x0024, char properties: 0x08, char value handle: 0x0025, uuid: 0000152b-1212-efde-1523-785feabcd123
handle: 0x0027, char properties: 0x0a, char value handle: 0x0028, uuid: 0000152c-1212-efde-1523-785feabcd123
handle: 0x002a, char properties: 0x02, char value handle: 0x002b, uuid: 0000152d-1212-efde-1523-785feabcd123
handle: 0x002d, char properties: 0x08, char value handle: 0x002e, uuid: 0000152e-1212-efde-1523-785feabcd123

[A0:E6:F8:1E:58:57][LE]> char-desc 000c 002f
handle: 0x000c, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x000d, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x000e, uuid: 00001524-1212-efde-1523-785feabcd123
handle: 0x000f, uuid: 00002901-0000-1000-8000-00805f9b34fb
handle: 0x0010, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0011, uuid: 00001526-1212-efde-1523-785feabcd123
handle: 0x0012, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x0013, uuid: 00002901-0000-1000-8000-00805f9b34fb
handle: 0x0014, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0015, uuid: 00001527-1212-efde-1523-785feabcd123
handle: 0x0016, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x0017, uuid: 00002901-0000-1000-8000-00805f9b34fb
handle: 0x0018, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0019, uuid: 00001528-1212-efde-1523-785feabcd123
handle: 0x001a, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x001b, uuid: 00002901-0000-1000-8000-00805f9b34fb
handle: 0x001c, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x001d, uuid: 00001529-1212-efde-1523-785feabcd123
handle: 0x001e, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x001f, uuid: 00002901-0000-1000-8000-00805f9b34fb
handle: 0x0020, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0021, uuid: 0000152a-1212-efde-1523-785feabcd123
handle: 0x0022, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x0023, uuid: 00002901-0000-1000-8000-00805f9b34fb
handle: 0x0024, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0025, uuid: 0000152b-1212-efde-1523-785feabcd123
handle: 0x0026, uuid: 00002901-0000-1000-8000-00805f9b34fb
handle: 0x0027, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0028, uuid: 0000152c-1212-efde-1523-785feabcd123
handle: 0x0029, uuid: 00002901-0000-1000-8000-00805f9b34fb
handle: 0x002a, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x002b, uuid: 0000152d-1212-efde-1523-785feabcd123
handle: 0x002c, uuid: 00002901-0000-1000-8000-00805f9b34fb
handle: 0x002d, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x002e, uuid: 0000152e-1212-efde-1523-785feabcd123
handle: 0x002f, uuid: 00002901-0000-1000-8000-00805f9b34fb

Lot of things here: 10 characteristics and 36 handles!

The first handle (0x000c) just marks the begin of the service (UUID 2800).

Then we see that there are 10 handles with UUID 2803 (Characteristic Declaration Attribute) and also 10 handles with UUID 2901 (Characteristic User Description) – these handles mark the begin of each characteristic and also defines its properties, so let’s read them all  (char-read-hnd) and translate it:

Name Char               0x000e  read+write
Button Char             0x0011  notify+read
Port Type Char          0x0015  notify
Low Voltage alert       0x0019  notify+read
High Current alert      0x001d  notify+read
Low Signal alert        0x0021  notify+read
Turn off device         0x0025  write
Vcc port control        0x0028  read+write
Battery type Indicator  0x002b  read
Disconnect Char         0x002e  write

Some characteristics also have an handle with UUID 2902 (Client Characteristic Configuration). That’s used for Notifications – the 5 characteristics that support this feature use this extra handle to activate or deactivate it.

So let’s read the 7 handles that are readable:

Name Char               75 30 30 30 31 = u0001
Button Char             00
Low Voltage alert       00
High Current alert      00
Low Signal alert        00
Vcc port control        01
Battery type Indicator  00

So ‘Name char’ contains the friendly name of my Hub. And it is writable, so what happens if I change it?

[A0:E6:F8:1E:58:57][LE]> char-write-cmd 0x000e 4d616a6f72
[A0:E6:F8:1E:58:57][LE]> char-read-hnd 0x00e
Characteristic value/descriptor: 4d 61 6a 6f 72

Well it retains what I wrote.

And if I now read the ‘Device Name’ it is no longer ‘u0001’:

[A0:E6:F8:1E:58:57][LE]> char-read-uuid 0x2A00
handle: 0x0003      value: 4d 61 6a 6f 72

So now my Hub announces a new name:

$ sudo hcitool -i hci0 lescan
LE Scan ...
A0:E6:F8:1E:58:57 (unknown)
A0:E6:F8:1E:58:57 Major

Changing 'Device Name' attribute

Cool, isn’t it?

Now the ‘Button Char’ just reads zero. But if I press the Hub’s button while I read it:

[A0:E6:F8:1E:58:57][LE]> char-read-hnd 0011
Characteristic value/descriptor: 01

it reads ‘1’.

And what about ‘Port Type Char’? It’s not defined as readable (just ‘notify’) but I can try to read it anyway:

[A0:E6:F8:1E:58:57][LE]> char-read-hnd 0015
Characteristic value/descriptor:

Hmm… null value.

But I have nothing connected to the Hub. Let’s try it again with a motor at port 1:

[A0:E6:F8:1E:58:57][LE]> char-read-hnd 0015
Characteristic value/descriptor: 01 01 00 01 01 00 00 00 01 00 00 00

And removing the motor again:

[A0:E6:F8:1E:58:57][LE]> char-read-hnd 0015
Characteristic value/descriptor: 01 00

Interesting – it’s not null now.

Let’s do the same again but on port 2:

[A0:E6:F8:1E:58:57][LE]> char-read-hnd 0015
Characteristic value/descriptor: 02 01 01 01 01 00 00 00 01 00 00 00

[A0:E6:F8:1E:58:57][LE]> char-read-hnd 0015
Characteristic value/descriptor: 02 00

So the first byte seems to identify the Port (’01’ or ’02’) and the second byte the action (’01’ might be ‘device plugged’ and ’00’ might be ‘device removed’). I have no explanation for the third byte and the other 8 bytes bytes seem to be some kind of identification of the motor.

Now let’s connect a tilt sensor in port 1 (and nothing in port 2):

01 01 00 22 00 00 00 10 00 00 00 10

and then move it to port 2:

02 01 01 22 00 00 00 10 00 00 00 10

Now just a distance sensor in port 1:

01 01 00 23 00 00 00 10 00 00 00 10

and move it to port 2:

02 01 01 23 00 00 00 10 00 00 00 10

Now just the motor again in port 1 and then the tilt sensor in port 2 (two readings):

01 01 00 01 01 00 00 00 01 00 00 00
02 01 01 22 00 00 00 10 00 00 00 10

So ‘Port Type Char’ doesn’t describe the ports state, it just shows the last action detected at ports. I think that’s the reason it’s not defined as ‘notify + read’, it’s purpose is just to notify that a change ocurred on ports.

Now we have 3 characteristics named “Alert”: Low Voltage Alert, High Current Alert and Low Signal Alert. Their names seem clear enough to understand their purpose and since all read zero everything seems to be OK. But let’s try to force an “High Current Alert” – make the motor run, block it and read the handle:

[A0:E6:F8:1E:58:57][LE]> char-read-hnd 001d
Characteristic value/descriptor: 00 
[A0:E6:F8:1E:58:57][LE]> char-read-hnd 001d
Characteristic value/descriptor: 00 
[A0:E6:F8:1E:58:57][LE]> char-read-hnd 001d
Characteristic value/descriptor: 00 
[A0:E6:F8:1E:58:57][LE]> char-read-hnd 001d
Characteristic value/descriptor: 00 
[A0:E6:F8:1E:58:57][LE]> char-read-hnd 001d
Characteristic value/descriptor: 01 
[A0:E6:F8:1E:58:57][LE]> char-read-hnd 001d
Characteristic value/descriptor: 01 
[A0:E6:F8:1E:58:57][LE]> char-read-hnd 001d
Characteristic value/descriptor: 01 
[A0:E6:F8:1E:58:57][LE]> char-read-hnd 001d
Characteristic value/descriptor: 00

When I block the motor with my hand, the Hub beeps and flashes the LED twice (in yellow). And sometimes the Alert reads ’01’.

But instead of reading several times the handler, we can activate ‘Notifications’ by turning ON the ‘notification’ flag in the ‘Client Characteristic Configuration’ and then just wait for the Hub to send the notification when the High Current Alert changes it’s state:

[A0:E6:F8:1E:58:57][LE]> char-write-req 001e 0100 -listen
Characteristic value was written successfully
[A0:E6:F8:1E:58:57][LE]> char-write-cmd 003d 01010164
Notification handle = 0x001d value: 01 
Notification handle = 0x001d value: 00

We used a different write command: ‘char-write-req’ instead of ‘char-write-cmd’ expects an answer from the device and ‘-listen’ keeps listening for future ‘answers’.

Now the ‘Vcc port control’ name isn’t much clear about it’s purpose. It reads ‘1’ but it’s also writable… does it allows us to control power in the ports? No matter what I write to 0x003d it seems to retain only ‘0’ or ‘1’ and when I write ‘0’ to it while the motor is running it keeps running.

‘Battery type indicator’ reads zero, I guess that it identifies the kind of battery being used. I’m using 2 AA alcaline batteries, will try again with the LiPo battery when I get it.

And finally the 2 writable-only characteristics: ‘Disconnect Char’ and ‘Turn off device’. They also seem quite clear about their purpose:

[A0:E6:F8:1E:58:57][LE]> char-write-cmd 002e 01
[A0:E6:F8:1E:58:57][LE]> 
(gatttool:8257): GLib-WARNING **: Invalid file descriptor.

[A0:E6:F8:1E:58:57][LE]> connect
Attempting to connect to A0:E6:F8:1E:58:57
Connection successful

Writing to ‘Disconnect Char’ makes the Hub disconnects but keeps it in discovery mode so I cpuld reconnect to it.

[A0:E6:F8:1E:58:57][LE]> char-write-cmd 0025 01
[A0:E6:F8:1E:58:57][LE]> 
(gatttool:8257): GLib-WARNING **: Invalid file descriptor.

[A0:E6:F8:1E:58:57][LE]> connect
Attempting to connect to A0:E6:F8:1E:58:57
[A0:E6:F8:1E:58:57][LE]> 
Error: connect error: Connection refused (111)

Writing to ‘Turn off device’ really turns it off.

So that’s it for ‘Nordic LED button service’. What LED, you ask? Well, the Android App is from Nordic and they have some examples on the Net with this UUID so my explanation is that LEGO used part of their code for the ‘button’ part without changing the UUID so the App recognizes it this way.

Next post: Motors and LED.

LEGO WeDo 2.0 – playing sound

Este artigo é a parte 4 de 6 da série  WeDo 2.0 - reverse engineering

Great news – LEGO Eduction released the WeDo 2.0 SDK today!

After digging into it, I found the information needed to control the Piezo: as expected, it’s controlled by the same handle that is used for controlling the motor and the RGB LED (0x003d). The “port” is “05” and the “command” to activate the Piezo is “02”, followed by a payload of “04” bytes containing:

  • the Frequency in Hz (2 bytes, reversed)
  • the duration in ms (2 bytes, reversed)

So to play a “C” (or “Do”, 261 Hz) during 1/8 of a second (125 ms) we use this command:

[A0:E6:F8:1E:58:57][LE]> char-write-cmd 003d 050204B801E803

So let’s hear the very first music played by a WeDo 2.0 from a linux shell script:

#!/usr/bin/env bash

# In Ubuntu run this script with sudo
# "Imperial March on a WeDo 2.0" was inspired by https://gist.github.com/tagliati/1804108

# command: gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204

# beep(a, 500) 
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B801F401
sleep 0.5

# beep(a, 500) 
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B801F401
sleep 0.5

# beep(a, 500) 
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B801F401
sleep 0.5

# beep(f, 350)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502045D015E01
sleep 0.35

# beep(cH, 150)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502040B029600
sleep 0.15

# beep(a, 500)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B801F401
sleep 0.5

# beep(f, 350)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502045D015E01
sleep 0.35

# beep(cH, 150)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502040B029600
sleep 0.15

# beep(a, 1000)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B801E803
sleep 1.0

# beep(eH, 500)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502049302F401
sleep 0.5
    
# beep(eH, 500)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502049302F401
sleep 0.5

# beep(eH, 500)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502049302F401
sleep 0.5

# beep(fH, 350) 
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204BA025E01
sleep 0.35

# beep(cH, 150)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502040B029600
sleep 0.15

# beep(gS, 500)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502049F01F401
sleep 0.5    

# beep(f, 350)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502045D015E01
sleep 0.35

# beep(cH, 150)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502040B029600
sleep 0.15

# beep(a, 1000)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B801E803
sleep 1.0
 
# beep(aH, 500)   
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502047003F401
sleep 0.5

# beep(a, 350) 
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B8015E01
sleep 0.35

# beep(a, 150)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B8019600
sleep 0.15

# beep(aH, 500)   
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502047003F401
sleep 0.5

# beep(gSH, 250)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502043E03FA00
sleep 0.25

# beep(gH, 250)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502041003FA00
sleep 0.25

# beep(fSH, 125)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204E4027D00
sleep 0.125

# beep(fH, 125) 
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204BA027D00
sleep 0.125  
   
# beep(fSH, 250)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204E402FA00
sleep 0.25

# delay(250)
sleep 0.25

# beep(aS, 250)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204C701FA00
sleep 0.25

# beep(dSH, 500)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502046E02F401
sleep 0.5

# beep(dH, 250)  
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502044B02FA00
sleep 0.25

# beep(cSH, 250)  
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502042A02FA00
sleep 0.25

# beep(cH, 125)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502040B027D00
sleep 0.125

# beep(b, 125)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204D2017D00
sleep 0.125

# beep(cH, 250)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502040B02FA00
sleep 0.25
      
# delay(250)
sleep 0.25

# beep(f, 125)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502045D017D00
sleep 0.125

# beep(gS, 500)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502049F01F401
sleep 0.5

# beep(f, 375) 
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502045D017701
sleep 0.375

# beep(a, 125)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B8017D00
sleep 0.125
  
# beep(cH, 500)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502040B02F401
sleep 0.5

# beep(a, 375)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B8017701
sleep 0.375

# beep(cH, 125)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502040B027D00
sleep 0.125

# beep(eH, 1000)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502049302E803
sleep 1.0

# beep(aH, 500)   
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502047003F401
sleep 0.5

# beep(a, 350) 
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B8015E01
sleep 0.35

# beep(a, 150)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B8019600
sleep 0.15

# beep(aH, 500)   
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502047003F401
sleep 0.5

# beep(gSH, 250)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502043E03FA00
sleep 0.25

# beep(gH, 250)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502041003FA00
sleep 0.25

# beep(fSH, 125)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204E4027D00
sleep 0.125
    
# beep(fH, 125) 
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204BA027D00
sleep 0.125

# beep(fSH, 250)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204E402FA00
sleep 0.25

# delay(250)
sleep 0.25

# beep(aS, 250)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204C701FA00
sleep 0.25

# beep(dSH, 500)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502046E02F401
sleep 0.5

# beep(dH, 250)  
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502044B02FA00
sleep 0.25

# beep(cSH, 250)  
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502042A02FA00
sleep 0.25

# beep(cH, 125)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502040B027D00
sleep 0.125

# beep(b, 125)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204D2017D00
sleep 0.125

# beep(cH, 250)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502040B02FA00
sleep 0.25

# delay(250)
sleep 0.25

# beep(f, 250)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502045D01FA00
sleep 0.25

# beep(gS, 500)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502049F01F401
sleep 0.5 
  
# beep(f, 375)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502045D017701
sleep 0.375

# beep(cH, 125)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502040B027D00
sleep 0.125
           
# beep(a, 500)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B801F401
sleep 0.5

# beep(f, 375)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 0502045D017701
sleep 0.375

# beep(c, 125)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 05020405017D00
sleep 0.125

# beep(a, 1000)
gatttool -i hci0 -b A0:E6:F8:1E:58:57 --char-write-req -a 0x003d -n 050204B801E803
sleep 1.0
 

 

 

WeDo 2.0 colors with python (again)

Este artigo é a parte 5 de 6 da série  WeDo 2.0 - reverse engineering

After some head aches with the WeDo 2.0 SDK I found out that the WeDo 2.0 Hub has 2 modes for the RGB LED device:

  • Indexed
  • Absolute

“Indexed” is the one I used before – only 10 colors are available. This the mode used in the WeDo 2.0 App, and when in this mode the command used to write to the RGB LED accepts only one byte as argument, which works as an “index” to 10 predefined colors. Why only 10 if the same byte can address up to 255 colors? Internal memory limitations?

The same command accepts  also 3 one-byte arguments (Red, Green and Blue) but only when the RGB LED mode is “Absolute”. This is clear in the SDK… what is not so clear is how to change from default (power on) Indexed mode to Absolute?

After reading many Java files I found out how: we use the “Input Command” characteristic (handle 0x3a) and send it this command:

0102061701010000000201

I’ll explain the format of the “Input Command” in another post, but for now this is the meaning:

  • first two bytes (0102) is the header used to set definitions
  • the third is the port of the device (06 is the RGB LED)
  • the fourth is the type of the device (17h a RGB LED)
  • the fifth is the mode (01 is Absolute, 00 is Indexed)
  • the sixth to nineth bytes is the delta for notifications to be noticed (so 01 00 00 00 = 1d is the minimum)
  • the tenth byte is the unit format to be used (02 is “SI”, internation standard)
  • the eleventh and last byte is to disable or enable notifications (01)

So we can now use any of the 16777216 color tones available:

The pyhton script used for the video above (the video just shows a small part)

#!/usr/bin/python
# run with sudo

from gattlib import GATTRequester
from time import sleep

BTdevice = "hci0"    # BlueTooth 4.0 BLE capable device
WeDo2HubAddress  = "A0:E6:F8:1E:58:57"
InputCommand_hnd = 0x3a
OutputCommand_hnd  = 0x3d
RGBAbsoluteMode_cmd = str(bytearray([01,02,06,17,01,01,00,00,00,02,01]))
RGBAbsoluteOutput_cmd = str(bytearray([06,04,03]))

DELAY      = 0.3

req = GATTRequester(WeDo2HubAddress,True,BTdevice)
sleep(DELAY)

# configure RBG LED to Absolute Mode
req.write_by_handle(InputCommand_hnd,RGBAbsoluteMode_cmd)

# loop all colors
while True:
  for blue in range (0,256,16):
    for green in range (0,256,16):
      for red in range (0,256,16):
        req.write_by_handle(OutputCommand_hnd, RGBAbsoluteOutput_cmd+chr(red)+chr(green)+chr(blue))

 

WeDo 2.0 Tilt Sensor

Este artigo é a parte 6 de 6 da série  WeDo 2.0 - reverse engineering

 

 

Thanks to Google, I already knew how to set the Tilt Sensor to «mode 1» and then read it. Unfortunately I was not able to understand the data format used by the sensor until I read the recently released SDK.

So the Tilt Sensor has 3 operating modes:

  • Angle (the default mode) = 0
  • Tilt = 1
  • Crash = 2

In this post I’ll explain how to read just the Tilt Mode as is similar to the original WeDo Tilt Sensor operating mode (and also because I still have to organize all my SDK notes).

The WeDo 2.0 ports can also be configured to read analog values in 3 formats: RAW (0), PERCENTAGE (1) and SI (2). Again, I will use just the SI format.

To change the operating mode to Tilt Mode we write this command to the Input Command characteristic (0x003a):

0102012201010000000201

The meaning is similar to the command used in last post for setting the RGB LED to Absolute Mode, the only difference is the Device Type (22h is the Tilt Sensor).

In Tilt Mode and SI Format there are 6 possible values we can read from the Tilt Sensor:

TILT_SENSOR_DIRECTION_NEUTRAL = 0
TILT_SENSOR_DIRECTION_BACKWARD = 3
TILT_SENSOR_DIRECTION_RIGHT = 5
TILT_SENSOR_DIRECTION_LEFT = 7
TILT_SENSOR_DIRECTION_FORWARD = 9
TILT_SENSOR_DIRECTION_UNKNOWN = 10

These values are read from the SensorValue characteristic (0x0032) in a 6-byte format where the first 2 bytes are always “0201” and the other 4 bytes are a IEEE754 float format representation of the value – and now I finally know how to understand those values!

So with just a crude python script we can use the WeDo 2.0 Hub with a Tilt Sensor to control a Mindstorms EV3 car:

#!/usr/bin/env python

# run with sudo

from ev3dev.auto import *
from gattlib import GATTRequester
from time import sleep

BTdevice = "hci0"       # BlueTooth 4.0 BLE capable device

WeDo2HubAddress  = "A0:E6:F8:1E:58:57"

SensorValue_hnd  = 0x32
InputCommand_hnd = 0x3a

# assume TiltSensor at Port #1, change to Mode 1 (Tilt) and Unit = SI
# TiltCmdMode1= "0102012201010000000201"
TiltCmdMode1 = str(bytearray([01,02,01,22,01,01,00,00,00,02,01]))

# Sensor value should read:
# "0201" + a 4 byte value (LITTLE_ENDIAN)
# representing a float in IEEE754 format
# http://www.h-schmidt.net/FloatConverter/IEEE754.html

TILT_SENSOR_DIRECTION_NEUTRAL  = "0000" # 0
TILT_SENSOR_DIRECTION_BACKWARD = "4040" # 3
TILT_SENSOR_DIRECTION_RIGHT    = "a040" # 5
TILT_SENSOR_DIRECTION_LEFT     = "e040" # 7
TILT_SENSOR_DIRECTION_FORWARD  = "1041" # 9
TILT_SENSOR_DIRECTION_UNKNOWN  = "2041" # 10

DELAY      = 0.3
STEP       = 0.175
DT         = 75

tilt_data=[0,0,0,0,0,0]

m1 = LargeMotor(OUTPUT_A)
m2 = LargeMotor(OUTPUT_B)

m1.run_direct()
m2.run_direct()
m1.duty_cycle_sp=0
m2.duty_cycle_sp=0

req = GATTRequester(WeDo2HubAddress,True,BTdevice)
sleep(DELAY)


# try reset - none works
# req.write_by_handle(InputCommand_hnd,"\x22\x44\x11\xAA")
# req.write_by_handle(InputCommand_hnd,"\x44\x11\xAA")
# sleep(DELAY)

# Configure Tilt Sensor
req.write_by_handle(InputCommand_hnd,TiltCmdMode1)
sleep(DELAY)

while True:
  # read Sensor
  data=req.read_by_handle(SensorValue_hnd)[0]

  if data:
    for i,b in enumerate(data):
      tilt_data[i]=ord(b)

    if (tilt_data[4] == 0x00) and (tilt_data[5] == 0x00):
      print("NEUTRAL")
    elif (tilt_data[4] == 0x40) and (tilt_data[5] == 0x40):
      print("BACKWARD")
      m1.duty_cycle_sp=-DT
      m2.duty_cycle_sp=-DT
      sleep(STEP)
      m1.duty_cycle_sp=0
      m2.duty_cycle_sp=0
    elif (tilt_data[4] == 0xa0) and (tilt_data[5] == 0x40):
      print("RIGHT")
      m1.duty_cycle_sp=-DT
      m2.duty_cycle_sp=DT
      sleep(STEP)
      m1.duty_cycle_sp=0
      m2.duty_cycle_sp=0
    elif (tilt_data[4] == 0xe0) and (tilt_data[5] == 0x40):
      print("LEFT")
      m1.duty_cycle_sp=DT
      m2.duty_cycle_sp=-DT
      sleep(STEP)
      m1.duty_cycle_sp=0
      m2.duty_cycle_sp=0
    elif (tilt_data[4] == 0x10) and (tilt_data[5] == 0x41):
      print("FORWARD")
      m1.duty_cycle_sp=DT
      m2.duty_cycle_sp=DT
      sleep(STEP)
      m1.duty_cycle_sp=0
      m2.duty_cycle_sp=0
    elif (tilt_data[4] == 0x20) and (tilt_data[5] == 0x41):
      print("UNKNOWN")

  else:
    print("No data")

  sleep(0.1)

Just a warning note: I ran a first version of this script in my Ubuntu laptop (without the EV3 parts) and it always fails at first run (“No data”, no matter what I do with the Tilt Sensor) but if I abort the script and run it again while the Hub is still in discovering mode then it always works. But when the same script runs in the EV3 it almost always works at first try.

And of course I do have a good explanation: gremlins! What else?