grack.com

Blog

Hacking Bluetooth to Brew Coffee from GitHub Actions: Part 2 - Reverse Engineering

This is part 2 of a three-part series covering the odyssey of getting a new coffeemaker, learning BTLE and how it works, reverse-engineering the Bluetooth interface and Android applications for the coffeemaker, writing a Rust-based CLI interface, and finally, hooking it all up to a GitHub actions bot that lets you brew a coffee just by filing an issue!

In part 1 we got our coffeemaker brewing using a sniffed command that we logged from the actual application, and then sent to the coffeemaker using a small Rust program. However, we don’t really understand the language we’re speaking yet, we’re just repeating the application-to-device babbling we’ve snooped.

Understanding the Packets

Now that we know that we can send a request, we want to understand what the format of the request looks like. The first thing we want to do is understand what a packet is. A packet is a chunk of data of a defined length, in contrast to a stream of data that continues indefinitely. Packets are used throughout most communication technologies and are a fundamental way of describing discrete communication messages.

When dealing with embedded devices, packets will almost always have a header, and sometimes a footer. The header and footer are called the framing of the packet, and they delimit it so we can identify exactly where it starts and stop.

Inside the header and footer might be things like start-of-packet, or end-of-packet markers, and a length for framing. There may also be additional metadata like a checksum to detect corruption.

Why is this framing important? Devices will often use framing to help recover from corruption. If you lose or corrupt a byte anywhere in the packet, you can often recover synchronization quickly by just restarting the packet parsing at the next byte that looks like a start byte.

Here’s a few packets we captured being sent from the coffeemaker to the application while asking it to brew a coffee, and then waiting for it to finish cleaning:

0d0575f0c4d5                             # Some sort of status request
0d1483f007010100410900be02030c001c0206dc # Brew a cappuccino
0d0883f00702062f41                       # Cancel brewing

d00783f0010064d9                         # Response to brew/stop request
d012750f02040100400700000000000000d621   # Status response
d012750f04050100400c030900000000001cf0   # Status response
d012750f000000000000036400000000009080   # Status response

What information can we glean from this? First of all, the first byte is always 0d or d0 (13 or 240 in decimal), suggesting this is a start-of-packet byte that varies depending on the direction of communication. That’s one byte probably identified!

+> 0d 0575f0c4d5
+> 0d 1483f007010100410900be02030c001c0206dc
+> 0d 0883f00702062f41
|
+> d0 0783f0010064d9
+> d0 12750f02040100400700000000000000d621
+> d0 12750f04050100400c030900000000001cf0
+> d0 12750f000000000000036400000000009080
|
+--------------------------------------- Start of packet (0x0d or 0xd0)

Next, the second byte of the packet seems to vary depending on the length of the packet, and it corresponds exactly with the change in packet size. This is highly likely to be a length, and from what we can see here in a couple of the packets we captured earlier, it would be the length of the packet not including the start-of-packet byte.

   v---5 bytes--v
0d 05 75 f0 c4 d5
   v------7 bytes-----v
d0 07 83 f0 01 00 64 d9
   v------------------18 bytes (0x12)------------------v
d0 12 75 0f 02 04 01 00 40 07 00 00 00 00 00 00 00 d6 21
^  ^
|  +------------------------------------------------------ Length of packet
+--------------------------------------------------------- Start of packet (0xd0)

We can’t glean much about the rest of the packet yet, but we’re getting some of the framing nailed down here. Time to pull out some more analysis tools.

There are three approaches we can use to understand the binary language of Delonghi’s ECAM machines:

  1. We can disassemble the firmware of the coffeemaker and understand what it expects and what it sends, or
  2. We can observe the application’s communication with the coffeemaker over a period of time, changing one or two things at a time and seeing what changes in the protocol, or
  3. We can disassemble the application that controls the coffeemaker and understand its inputs and outputs.

The firmware of the machine itself would be the ideal place for us to look, but according to some various coffeemaker-hacking forums, the controllers are PIC-based, and disassembling/dumping PIC firmware somewhat tricky.

In addition, a disadvantage to disassembling microcontroller firmware is that due to size constraints, it’s far less likely for text strings to have survived the compilation process to give us hints as to what’s going on. Finding leftover snippets of logging or “debug” print statements are gold for the reverse engineer, and we’d like to use that as a signpost to guide our future work.

Observing the application’s communication directly is definitely an option. This is inconvenient as we saw from the HCI snooping adventures earlier on, and we might not know how to perturb the system enough to fully understand most of the fields we receive.

The best option we’re left with is disassembling the application itself and looking for hints as to what it’s doing, hopefully for some symbols that give us names, or text strings that may give us context.

Disassembling the Delonghi APK

We’re going to disassemble the Delonghi APK to learn more about how we can automate our caffeine fix.

Android applications are shipped in APK (Android Package Kit) format, and we’re going to download a few historical versions of the APK from APK Pure, a site that archives older versions of shipped applications. Getting a few different versions is a good idea, as developers will sometimes forget to enable obfuscation in some versions. If we’re lucky enough to get a version of the application without obfuscation, we can get the internal names for constants and fields.

In the past, APK decompilation was somewhat tricky. As Android is Java-based, but doesn’t use Java’s bytecode directly, either you’d need to learn how to understand smali, or you’d use dex2jar to convert the app to a faux-Java JAR file and use standard Java analysis tools to reverse engineer it. Jadx is a new Java analysis tool, which is far easier to use and much more powerful than the older tools.

Let’s open the APK in Jadx. Once it has decompiled that app, the first thing we’ll notice is that there are package names here. This is great news and suggests that even if the application is obfuscated, it’s not obfuscated fully and we’ll be able to learn how it ticks.

When we dig into some of the classes, we see method and field names, showing us a pretty clear representation of the original source code. Even better news!

After decompiling, our first goal should be to conclusively identify the framing of the packets and answer the questions we raised earlier on. With some reading through the source, we can identify the source of one of the packets we saw being sent to the machine…

0d 05 75 f0 c4 d5

… as coming from here:

public static byte[] getByteMonitorMode(int i) {  
    String str = TAG;  
    DLog.m188e(str, "getByteMonitorMode  dataN" + i);  
    byte[] bArr = new byte[6];  
    bArr[0] = 0xd;                            // ** 0d
    bArr[1] = 5;                              // ** 05
    if (i == 0) {  
        bArr[2] = DATA_0_ANSWER_ID;  
    } else if (i == 1) {  
        bArr[2] = DATA_1_ANSWER_ID;  
    } else if (i == 2) {  
        bArr[2] = 117;                        // ** 75
    }  
    bArr[3] = 0xf0;                           // ** f0
    int checksum = checksum(bArr);  
    bArr[4] = (byte) ((checksum >> 8) & 255); // ** c4 
    bArr[5] = (byte) (checksum & 255);        // ** d5
    return bArr;  
}

We can see the canonical source for every byte in that packet above in this function. 0d is the start_of_packet header. 05 is the length, and it looks like it’s just hardcoded here since the packet is always the same length. We can also see that the last few digits are a checksum, and if we dig into the checksum function, how it is calculated:

public static int checksum(byte[] bArr) {  
    int i = 7439;  
    for (int i2 = 0; i2 < bArr.length - 2; i2++) {  
        int i3 = (((i << 8) | (i >>> 8)) & 65535) ^ (bArr[i2] & 255);  
        int i4 = i3 ^ ((i3 & 255) >> 4);  
        int i5 = i4 ^ ((i4 << 12) & 65535);  
        i = i5 ^ (((i5 & 255) << 5) & 65535);  
    }  
    return i & 65535;  
}

Great! checksum looks like it could be one of the CRC family of functions, but we don’t necessarily have to fully understand it yet if we have its implementation. We now have all the framing necessary to construct any packet:

d0 LE (data) C1 C2
^  ^         ^--^-- Our checksum bytes   
+  +--------------- Length of packet, minus start-of-packet
+------------------ Start of packet (0xd0 or 0x0d depending on direction)

Now we can start to guess at the meaning of the rest of the bytes. Byte 2 appears to be a command ID. Byte 3 is a constant, and scanning the rest of the file suggests that it’s always 0x0f or 0xf0, depending on the command. That leaves the remainder of the packet for the command payload, if it’s used for the command.

0d 05 75 f0 c4 d5
^  ^  ^  ^  ^--^-- Our checksum
|  |  |  +-------- Always 0xf0 or 0x0f
|  |  +----------- The command ID (0x75 = monitor mode 2)
|  +-------------- The packet length
+----------------- Start of packet (0x0d)

Since we have a single spot for calculating the packet checksum, and we know that every request requires a checksum to be calculated, we can figure out all the places in the app that create request packets to get a better idea of what we can ask the machine to do by looking for the callers of checksum:

This is very interesting! There’s a lot of commands here, and each of them has a reasonably-well-defined name that we can use to understand what its function might be. We’ll have to start working through them one-by-one. With a bit of work, we can assemble a table of these commands:

enum EcamRequestId {
    SetBtMode = 17,
    MonitorV0 = 96,
    MonitorV1 = 112,
    MonitorV2 = 117,
    BeverageDispensingMode = 131,
    AppControl = 132,
    ParameterRead = 149,
    ParameterWrite = 144,
    ParameterReadExt = 161,
    StatisticsRead = 162,
    Checksum = 163,
    ProfileNameRead = 164,
    ProfileNameWrite = 165,
    RecipeQuantityRead = 166,
    RecipePriorityRead = 168,
    ProfileSelection = 169,
    RecipeNameRead = 170,
    RecipeNameWrite = 171,
    SetFavoriteBeverages = 173,
    RecipeMinMaxSync = 176,
    PinSet = 177,
    BeanSystemSelect = 185,
    BeanSystemRead = 186,
    BeanSystemWrite = 187,
    PinRead = 210,
    SetTime = 226,
}

Some of these request IDs are guesses based on the surrounding code context, and some of them are defined in enumerations in the application source. It’s a pretty good start for us to get going on figuring out how to brew our own beverage from scratch.

Revisiting the brew command

From just the bytes sent across the connection it’s difficult to understand exactly how the application is creating the packet to brew a coffee. However, in the disassembly we find a function dispenseBeveragePacket that appears to construct the packet we saw before:

if (arrayList != null) {  
    Iterator<ParameterModel> it3 = arrayList.iterator();  
    loop1: while (true) {  
        i4 = 0;  
        while (it3.hasNext()) {  
            next = it3.next();  
            if (next.getId() < 23 || next.getId() == 28) {  
                if (bool.booleanValue() || i != 200 || next.getId() != 2) {  
                    i6 = i6 + 2 + i4;  
                    bArr[i6 + 6] = (byte) next.getId();  
                    if (Utils.isTwoBytesShort(next.getId())) {  
                        bArr[i6 + 7] = (byte) (next.getDefValue() >> 8);  
                        bArr[i6 + 8] = (byte) next.getDefValue();  
                        i4 = 1;  
                    }  
                }  
            }  
        }  
        bArr[i6 + 7] = (byte) next.getDefValue();  
    }  
    i5 = i4;  
}

If we clean it up a bit to some Java pseudocode, it looks like this:

int index = 6;
for (ParameterModel param in params) {
    if (param.getId() < CLEAN_TYPE || param.getId() == ACCESSORIO) {
        array[index++] = param.getId();
        if (Utils.isTwoBytesShort(param.getId())) {
            array[index++] = param.getDefValue() >> 8;   // upper 8 bytes
            array[index++] = param.getDefValue() & 0xff; // 
        } else {
            array[index++] = param.getDefValue() & 0xff;
        }
    }
}

So, from this pseudocode we see that a beverage is constructed from a list of ingredients (which with some further investigation, we find in the disassembled source as IngredientsId below), and an associated one or two byte value (param.getDefValue() above). Digging through the source for which ingredients are one or two bytes doesn’t yield much fruit, but maybe we can understand what’s going on by investigating further.

Where do we go next? We find that there are two commands that, based on their name, seem to be related to the application UI used for brewing: RecipeQuantityRead and RecipeMinMaxSync.

Let’s try sending these commands to the machine! To do this, it’s time to re-visit our Rust code.

First, let’s create a function that will add the packet framing (header, length and checksum) to any payload we want to send:

pub fn checksum(buffer: &[u8]) -> [u8; 2] {
    let mut i: u16 = 7439;
    for x in buffer {
        let i3 = ((i << 8) | (i >> 8)) ^ (*x as u16);
        let i4 = i3 ^ ((i3 & 255) >> 4);
        let i5 = i4 ^ (i4 << 12);
        i = i5 ^ ((i5 & 255) << 5);
    }

    [(i >> 8) as u8, (i & 0xff) as u8]
}

fn packetize(buffer: &[u8]) -> Vec<u8> {
    let mut out = [&[
        0x0d,
        (buffer.len() + 3).try_into().expect("Packet too large"),
    ], buffer].concat();
    out.extend_from_slice(&checksum(&out));
    out
}

async fn run_with_peripheral(peripheral: Peripheral, characteristic: Characteristic) -> Result<(), Box<dyn std::error::Error>> {
    let data = packetize(/* data */);
    peripheral.write(&characteristic, data, WriteType::WithoutResponse);
    Ok(())
}

Now we can start sending some example packets and exploring the responses. Here’s two test packet’s we’ll send (in pseudo-code and byte form):

Packet                             Bytes
RecipeInfo(profile=1, beverage=7)  0d 07 a6f0 01 07 75c2
RecipeMinMaxInfo(beverage=7)       0d 06 b0f0 07 6af4

Let’s look at what comes back, in raw byte form (some spaces added to help the reader visualize the packet):

RecipeInfo(profile=1, beverage=7):       
d0 17 a6f0 01 07 
↳ 0100410900be02030c001b0419011c02
↳ a2cd

RecipeMinMaxInfo(beverage=7):
d0 2c b0f0 07
↳ 010014004100b409003c00be038402000305
↳ 18010101190101010c0000001c000200
↳ 1b000404
↳ d03c

The first part of the response packets appear to be the machine echoing back the input. This makes sense, as the application will need a way to match up responses to requests.

For the remainder of the packet, we have the advantage of the decompilation above. We know the list of ingredients from the IngredientsId enumeration in the decompiled source, and if we match up the type of beverages with the ingredients, it makes a lot of sense that it’s what we’re seeing here:

RecipeInfo response:

01·0041•09·00be•02·03•0c·00•1b·04•19·01•1c·02
---+--- ---+--- --+-- --+-- --+-- --+-- --+--  
   |       |      |     |     |     |     ╰---- Accessorio=2
   |       |      |     |     |     |          
   |       |      |     |     |     ╰---------- Visible=1
   |       |      |     |     |                
   |       |      |     |     ╰---------------- IndexLength=4
   |       |      |     |                      
   |       |      |     ╰---------------------- Inversion=0
   |       |      |                            
   |       |      ╰---------------------------- Taste=3
   |       |                                   
   |       ╰----------------------------------- Milk=190
   |                                           
   ╰------------------------------------------- Coffee=65

RecipeMinMaxInfo response:

01·0014·0041·00b4•09·003c·00be·0384•02·00·03·05•
--------+-------- --------+-------- -----+-----  
        |                 |              ╰------- Taste: 0<=3<=5
        |                 |                      
        |                 ╰---------------------- Milk: 60<=190<=900
        |                                        
        ╰---------------------------------------- Coffee: 20<=65<=180
> 18·01·01·01•19·01·01·01•0c·00·00·00•1c·00·02·00•
  -----+----- -----+----- -----+----- -----+-----  
       |           |           |           ╰------- Accessorio: 0<=2<=0
       |           |           |                   
       |           |           ╰------------------- Inversion: 0<=0<=0
       |           |                               
       |           ╰------------------------------- Visible: 1<=1<=1
       |                                           
       ╰------------------------------------------- Programmable: 1<=1<=1
> 1b·00·04·04
  -----+-----  
       ╰------- IndexLength: 0<=4<=4

From reading the application source, these packets seem to represent the current settings for the beverage, and the minimum and maximum ranges for each of the parameters. We can also start to guess at what the lengths of each of the ingredients’ parameter values are: most are a single byte, but a handful seem to be reliably two bytes wide and they all seem to deal with liquids.

Let’s confirm our intuition here by looking at the recipes for a latte and hot water:

Latte recipe:

01·003c•09·01f4•02·03•0c·00•1b·04•19·01•1c·02
---+--- ---+--- --+-- --+-- --+-- --+-- --+--  
   |       |      |     |     |     |     ╰---- Accessorio=2
   |       |      |     |     |     |          
   |       |      |     |     |     ╰---------- Visible=1
   |       |      |     |     |                
   |       |      |     |     ╰---------------- IndexLength=4
   |       |      |     |                      
   |       |      |     ╰---------------------- Inversion=0
   |       |      |                            
   |       |      ╰---------------------------- Taste=3
   |       |                                   
   |       ╰----------------------------------- Milk=500
   |                                           
   ╰------------------------------------------- Coffee=60

Hot water recipe:

0f·00fa•19·01•1c·01
---+--- --+-- --+--  
   |      |     ╰---- Accessorio=1
   |      |          
   |      ╰---------- Visible=1
   |                 
   ╰----------------- HotWater=250

We can see that the pattern continues here: liquid amounts are two bytes long, while everything else is a single byte.

If you’re curious about every recipe this machine can make, a gist is available with a dump of the recipes.

We can now put all the pieces together and build our own brewing command with a recipe that we’re going to write from scratch: a large cappuccino!

83 f0 07 01 01 00 78 09 01 77 02 04 02
^  ^  ^  ^  ^  ^  ^  ^  ^  ^  ^  ^  ^- Preparation mode = PREPARE
|  |  |  |  |  |  |  |  |  |  +--+---- Taste (02) = strong (value 4)
|  |  |  |  |  |  |  +--+--+---------- Milk (09) = 375
|  |  |  |  +--+--+------------------- Coffee (01) = 120
|  |  |  +---------------------------- Trigger=START
|  |  +------------------------------- Beverage=Cappuccino (value 7)
|  +---------------------------------- Always 0xf0
+------------------------------------- Beverage dispense request   

Let’s send that to the machine and see what happens:

Success! We have the most complicated and important part of communication with a coffeemaker working: brewing the coffee.

There’s a bit of work required to turn this into a full application, but you can find that pre-written in my longshot project on GitHub. You don’t need to have this coffeemaker, as it includes a simulator mode that will allow you to brew a virtual coffee and test out the packet parsing and generation code.

Continue reading… In part three of the series we’ll hook this up to GitHub Actions so that we can automate coffee brewing from the browser!

Further reading for this post

Follow me on Mastadon for more updates on this adventure!

Hacking Bluetooth to Brew Coffee from GitHub Actions: Part 1 - Bluetooth Investigation

This is going to be a long journey in three parts that covers the odyssey of getting a new coffeemaker, learning BTLE and how it works, reverse-engineering the Bluetooth interface and Android applications for the coffeemaker, writing a Rust-based CLI interface, and finally, hooking it all up to a GitHub actions bot that lets you brew a coffee just by filing an issue!

Introduction

I’ve always been pretty content with using whatever coffeemaker I had nearby. For the last two or three years I’ve been using an old 2-in-1 Breville YouBrew coffeemaker, with a grinder built-in. It was a workhorse and worked perfectly until this September. A few months ago the machine asked me to run the regular descaling process to deal with our hard water, and this is where our adventure starts.

For those of you without hard water, our water in Canada like that of Italy, tends to be hard due to the type of rocks that water passes through on the way into our water supply. Over time, the hard water minerals precipitate out and stick to the metallic pipes and heaters within the machine, causing the temperature of the brewing water to drop, and other quality problems. Regular descaling is recommended, and some modern machines feature special descaling modes and chemicals that make this easier to do.

Figure: Major and Trace Elements in Tap Water from Italy January 2012 Journal of Geochemical Exploration 112:54-75 DOI:10.1016/j.gexplo.2011.07.009

To descale a machine, you generally use a weak acid like vinegar, or a more complex descaling agent to dissolve the precipitate as salts so they can be flushed out. Unfortunately, the last time I ran the descaling process on this coffeemaker the acid I was using seems to have degraded one of the internal seals and the machine began to leak significantly.

Faced with the prospect of opening it up and actually figuring out what part was leaking, I was finally convinced that given the amount of coffee we drink, it was time for us to invest in a more modern and little more upscale coffeemaker.

Fortunately a few months earlier over the same Spring, my partner had been TA’ing a course at a building in a nearby hospital over the summer and came home raving about a machine they had, that let her brew coffee from an application: the Delonghi Dinamica Plus. We talked about how cool it was to brew from an app and how it could make so many different drinks, but we both forgot about it for a few months until our the old machine failed.

Our coffee situation was pretty dire and we decided to pull the trigger on a Delonghi Dinamica Plus, despite the eye-watering price. We waited anxiously for a week, keeping the old coffeemaker in service by putting it inside a tray to catch the leaks.

When the new coffeemaker arrived we set it up and – to avoid a major digression – the coffee it made was excellent. Tasty espresso, perfect americano, creamy cappuccino. There was one major problem: the application you’re supposed to use for the coffeemaker doesn’t reliably connect and stay connected to the machine.

The Dinamica Plus is about $100 more than the Dinamica, and this cost is effectively for the privilege of brewing coffee from your phone (along with some other goodies, like defining your favourite or custom drinks from the couch). The application is difficult to use and a bit buggy, however. It will often fail to find the coffeemaker. Once you’ve connected, there’s no guarantee that it’ll allow you to connect again without wiping all the saved data from the application. It’s also integrated with some sort of online service that returns a 404.

I found myself at a crossroads here: Do I accept that the extra features that I paid for will go to waste, or do I dig in and see if there’s some way I can get this feature to work in a way that I can actually use it. There’s a lot you can learn by being forced to dig into something new, and it looked like this might be an opportunity to understand a bit more about Bluetooth. So, as you can probably guess from the remaining length on this topic, I took the latter path.

BTLE Background and Traffic Sniffing

The first question we need to answer is how we’re going to talk to this thing. We know it’s Bluetooth from the application’s insistence that Bluetooth be turned on. But it’s not showing up on my MacBook’s Bluetooth devices list, which means it’s somehow different than the common Bluetooth “Classic” audio and phone devices that show up there.

There may be other reasons why the coffeemaker doesn’t show up in a list of Bluetooth devices on a laptop, but the most likely candidate for something that isn’t Bluetooth Classic like those devices is Bluetooth Low-Energy, or more commonly known as BTLE.

One of BTLE’s major fame was its use in iBeacon for the Apple ecosystem: a way of transmitting that a physical location was “interesting” in some way to some set of applications. This is done by “advertising” Apple’s Bluetooth SIG identifier, along with an ID that specifies you’re talking iBeacon language, and some metadata allowing you to identify two 16-bit numbers of data along with that.

BTLE devices are often hidden from general device users, and you access them through specific programs and applications. For example, if you’re looking to rent a scooter, the app might communicate directly with the scooter over BTLE to unlock it for use directly from the phone – no cellular radio required!

We can scan for BTLE devices using an app like nRF Connect that will show us all the nearby devices to our mobile phone. In the screenshot below you’ll see everything that speaks BTLE that my phone can see:

There’s no obvious coffeemaker here but we can start to make some educated guesses by using signal strength. We’ll walk from the couch to the coffeemaker and see that one of our BTLE devices shows an increasing signal level.

We can confirm that this is the right device by forcing a connection to it and seeing the same Bluetooth icon appear on the screen that also appears when the application connects.

Aha! So now we’ve proved this coffeemaker is communicating over BTLE. But let’s digress and dig into what BTLE actually is, so we can figure out how to talk to it.

At a high level, BTLE works around the concept of a Service and a Characteristic, both identified by UUIDs. A Service is a container for Characteristics and, at a high level, two services on two different devices sharing the same UUID will generally implement the same “protocol”. BTLE stacks on devices will generally allow you to scan for announcements of devices advertising a Service UUID you are interested in, allowing you to take inventory of supported devices relatively easily.

The Characteristic is an endpoint on the Service and has some directionality information embedded within it. A Characteristic at a high level provides READ and/or WRITE operations to communicate with the device, but also which of the devices in the connection can be the origin of this data (ie: does the device broadcast packets? will it send them unannounced? does the device only communicate if you send it something first?). Table 2 in this tutorial lists the options if you’re interested in more detail.

We’ve learned some important information along the way. From our scan earlier, we know the two bits we need to communicate with the device, ie: the Service UUID is 00035b03-58e6-07dd-021a-08123a000300 and the Characteristic UUID is 00035b03-58e6-07dd-021a-08123a000301.

What we need now is something to send to it. I’m not brave enough to brute force packets for an expensive coffeemaker, so let’s try to capture some real communication between the application and the device.

Android has something called the Bluetooth HCI snoop log that, as you can imagine, allows you to snoop on Bluetooth communication. HCI stands for “host-controller interface”, and we’ll use the snooper to log interaction between the phone and the coffeemaker. The process of enabling and retrieving HCI logs varies wildly between device models, so your mileage may vary.

We can tap the button in the app to brew a cappuccino and, from the logs, see what the application sends to the machine. We don’t have any context as to what these packets are, but we know that sending this will brew a cappuccino with the default parameters (which according to the application is 65ml of coffee, 19 seconds of milk, and “medium” aroma):

Brew a cappuccino:
0d 14 83 f0 07 01 01 00 41 09 00 be 02 03 0c 00 1c 02 06 dc

Device sends back:
d0 07 83 f0 01 00 64 d9

Cancel brewing:
0d 08 83 f0 07 02 06 2f 41

Device sends back:
d0 07 83 f0 01 00 64 d9

Communicating in Rust

Now we’re going to need to talk to the coffeemaker ourselves. Each platform has its own libraries for talking to Bluetooth devices (CoreBluetooth on OSX, bluez-over-DBUS for Linux, etc.) Because of the complexity of dealing with cross-platform Bluetooth, most platforms like node.js, Rust, and Python, provide libraries that abstract this away.

My first attempts to talk to the coffeemaker were using node.js. I tried noble, and bleno, but neither appeared to work for me on my Mac. I’ve been wanting to get my Rust skills back into shape, so I decided to explore the btleplug library which is being actively maintained on all modern platforms.

To connect to the device we’re interested in, first we need to start a scan to find devices that are advertising the Service and Characteristic we are interested in. The following code fragment will ask btleplug to scan for peripherals on every adapter connected to the system.

async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let manager = Manager::new().await?;
    let filter = ScanFilter {
        services: vec![SERVICE_UUID],
    };

    eprintln!("Looking for coffeemakers...");
    for adapter in manager.adapters().await? {
        adapter.start_scan(filter.clone()).await?;
        tokio::time::sleep(Duration::from_secs(10)).await;
        for peripheral in adapter.peripherals().await? {
            eprintln!("Found peripheral");
            peripheral.connect().await?;
            peripheral.discover_services().await?;
            for service in peripheral.services() {
                for characteristic in service.characteristics {
                    if service.uuid == SERVICE_UUID && characteristic.uuid == CHARACTERISTIC_UUID {
                        run_with_peripheral(peripheral.clone(), characteristic).await?;
                    }
                }
            }
        }
    }

    Ok(())
}

async fn run_with_peripheral(
    peripheral: Peripheral,
    characteristic: Characteristic,
) -> Result<(), Box<dyn std::error::Error>> {
    eprintln!("{:?}", characteristic);
    Ok(())
}

If we run this code it, prints:

Looking for coffeemakers...
Found peripheral
Characteristic { uuid: 00035b03-58e6-07dd-021a-08123a000301, service_uuid: 00035b03-58e6-07dd-021a-08123a000300, properties: READ | WRITE | INDICATE }

Looks good! We found the coffeemaker. And now we can try to send a raw packet to it that we had captured earlier that we believed was sent to brew cappuccino. Let’s change run_with_peripheral to this:

async fn run_with_peripheral(peripheral: Peripheral, characteristic: Characteristic) -> Result<(), Box<dyn std::error::Error>> {
    let data = &[0x0d,0x14,0x83,0xf0,0x07,0x01,0x01,0x00,
        0x41,0x09,0x00,0xbe,0x02,0x03,0x0c,0x00,0x1c,0x02,0x06,0xdc]; 
    peripheral.write(&characteristic, data, btleplug::api::WriteType::WithoutResponse);
    Ok(())
}

And hey, that started brewing a cappuccino!

Let’s take stock of where we are:

  • We know how to communicate with the device
  • We know the UUIDs of the endpoint that we’re going to communicate with
  • We can write a packet to the device and see that it performs an action

This might be enough to stop here. We could trace all the packets to brew all the coffee recipes we want, but that doesn’t seem like a complete solution.

Continue reading… In part two of the series we’ll reverse engineer the protocol itself!

Further reading for this post

Follow me on Mastadon for more updates on this adventure!

Opinionated Dress Color Simulator

Given the all the fuss over the blue/black or white/gold dress this week, I whipped up a quick simulation that allows you to view the dress under different lighting conditions.

Slide the control from side-to-side to fade between the three images: a “blue/black” version where we subtract out the theoretical color of the light (#524922), the original picture, and a “white/gold” version where we add color to compensate for the perceived shadow (#392c00).

qz.com picked this up and created their own version as well.

Grab the source from GitHub.

USB-C and Modular Smartphones Are the End-Game for Convergence

Two new technologies – the modular smartphone and USB 3.1 – are set to radically accelerate the convergence of everything to a single, in-pocket device. In fact, they are so revolutionary that they will change the entire way we think about mobile computing as the iPhone did earlier this millennium.

Modules will eat custom hardware

Modular smartphones, the first of which being Project Ara, will shift the way we buy devices while opening up new markets.

The backbone of Project Ara is UniPro – a standard for communication not unlike Intel’s Thunderbolt – with significant bandwidth, enough to push 4k video uncompressed between them. Modules are built on top of this high-speed network.

The current pipeline for a “revolutionary” add-on in the mobile space has many gatekeepers. When FLIR wanted to launch a smartphone IR camera, they needed to build an entire phone backpack around it.

Another company, Lytro, has no smartphone equivalent to their light-field camera. Instead, you need to buy an entire custom camera or point-and-shoot system which significantly adds to the cost of the system, and requires the end-user to commit to carrying a full, purpose-built device.

The same story is playing out in the construction space. Laser measuring tools are following in the same backpack space. Unsurprisingly, the same pattern of building significant packaging around simple components has played out for construction tools, battery cases, high-end cameras, and Tazers as well.

The obvious advantage to an Ara-like device is that manufacturers can bring a module to market without the traditional risk-averse gatekeepers in the manufacturer (Samsung, LG, etc.) and carriers (AT&T, Verizon, etc.). If the manufacturers decide that this custom module isn’t a fit for their mass-market devices, they’ll pass.

Rather than Lytro or a small Kickstarter team convincing one of the big names to include their custom module at significant risk to both parties, the team can produce the module themselves and directly market to end-users without the overhead of developing a bulky backpack add-on.

By subverting the manufacturer channels, this significantly de-risks the process of bringing new concepts to market and will open up a flood of novel ideas.

Module development provides another channel for manufacturers to rely on the smartphone to be the “brains” of their devices. Making a custom device OS and shell will no longer make sense for companies like FLIR and Fluke when building IR cameras and borescopes for a large part of their product line – at least the products that can be used outside of emergency situations. While this is possible now through Bluetooth or audio jack hacks like Ryobi’s phoneworks, making a module will make more sense.

USB’s type-C: one cable to rule them all

USB 3.1 and the type-C cable give us the ability to reliably connect a device to external components – even those requiring high bandwidth. By plugging in a single cable, you’ll have access to over 10 Gb/s of bandwidth for transmitting USB data. In addition, the type-C cable can be used in “alternate modes” like DisplayPort for pushing video outside of the USB protocol.

What USB-C will enable is the ultimate computing convergence. When you’re at home or work and need to use the “big” screen, along with keyboard and mouse, you’ll plug into a desktop dock. This is already something happening for MacBooks: you can plug a single Thunderbolt cable into a dock and get access to a wide variety of ports.

If you need a larger experience, but don’t want to be tied to a desk, the laptop dock will be your friend. Motorola’s Atrix was the first rough iteration of this idea. It required both a USB micro and HDMI connection and it was tied to a single device. With the USB type-C connector, we will have the ability to plug a single cable into an supported device while getting video out and pushing power and keyboard/trackpad in.

The Future

The era of the laptop and personal computer will eventually come to an end, but not until they allow developers and power users to do everything they can on high-end devices as easily on mobile devices. Before this can happen professionals of all sorts, the power users of the various platforms will need as much memory and processing power as they are currently using.

The type-C connector gets us part of the way there by moving graphics and input off your mobile device, but you’ll still be processing with whatever you can carry with you on your phone. While processor speeds will be improving the mobile space, it is likely that chips on larger boards will always be more powerful.

What we’ll see is the evolution of devices that can move computation from one place to another, in near-real-time. You’ll carry a modular smartphone on your person and have a more powerful version in desktop or laptop form. Plugging the device into the larger form factor will move your computation over to a more powerful CPU/GPU with significantly more RAM. You’ll bring your storage with you – the experience of computing will expand and contract depending on where you are and what you need.

We’re not far away from this right now. With applications running on JIT systems like Dalvik, the same code can be run on two different CPU types and the same techniques we use to juggle Javascript between interpreted and optimized states can transport an application between them.

Your pocket device will be the center of your digital life: a single mobile device when you’re on the go that can act as everything you need for work and play, an entertainment source and gaming console when you’re at home in front of your TV, the storage for your laptop when you need to work on the go, and the core of your desktop computing environment for the professionals who need high performance.

Thoughts? Follow me on Twitter @mmastrac and let me know.

Translations: Пост доступен на сайте softdroid.net.

What's new in CSS Selectors 4

Please note that this article is written about an Editor’s Draft of a specification as of January 2015, which means the information may change without notice

CSS Selectors Level 4 is the next iteration of the CSS selector spec, the last version of which was made a recommendation in 2011 after being a working draft for a number of years.

So, what’s new?

Selector Profiles

CSS selectors are now categorized into two groups: fast and complete. Fast selectors are those selectors appropriate for use in a dynamic CSS engine. Complete selectors are appropriate for use in cases where being as fast as possible isn’t necessarily a problem, document.querySelector, for instance.

Selectors are used in many different contexts, with wildly varying performance characteristics. Some powerful selectors are unfortunately too slow to realistically include in the more performance-sensitive contexts. To accommodate this, two profiles of the Selectors spec are defined [ref].

:has

:has is the most interesting part of CSS Selectors 4, but it comes with an important caveat described below. What it allows you to do is change the subject of the selector – i.e., the element that will actually be styled – while continuing to match elements that appear later in document order.

This opens up a great deal of new ways to match content. For instance, matching sections with a header:

// Any section that has a header element
section:has(h1, h2, h3, h4, h5, h6)

Or a developer can match all paragraphs that contain nothing but any number of images:

// Match a paragraph that does not have anything that is not an image
p
  :has(img)             // has an image
  :not(:has(:not(img))) // does not have anything not an image

Even matching an element that has a specific number of children (in this case, five):

// Sidebar with five children
div.sidebar
    :has(*:nth-child(5))       // Has a fifth child
    :not(:has(*:nth-child(6))) // But not a sixth child

Caveat: at this time the :has selector is not considered fast, which means that it may not be available for use in stylesheets. As nobody has implemented this selector yet, its performance characteristics are still an open question. If browser vendors can make it fast, it may be available for general styling as well.

In previous versions of the specification this was indicated using an exclamation mark (!) next to the subject – that syntax is now gone.

:matches

:matches is a standardization of :moz-any and :webkit-any that have existed with browser prefixes for some time. This allows a stylesheet author to collapse duplicate rule paths.

This will be useful for collapsing generated Cartesian-product-esque SCSS/SASS output like this:

  body > .layout > .body > .content .post p a.image.standard:first-child:nth-last-child(4) ~ a.image.standard, 
  body > .layout > .body > .content .post p a.image.standard:first-child:nth-last-child(4), 
  body > .layout > .body > .content .post li a.image.standard:first-child:nth-last-child(4) ~ a.image.standard, 
  body > .layout > .body > .content .post li a.image.standard:first-child:nth-last-child(4), 
  body > .layout > .body > .content .page p a.image.standard:first-child:nth-last-child(4) ~ a.image.standard, 
  body > .layout > .body > .content .page p a.image.standard:first-child:nth-last-child(4), 
  body > .layout > .body > .content .page li a.image.standard:first-child:nth-last-child(4) ~ a.image.standard, 
  body > .layout > .body > .content .page li a.image.standard:first-child:nth-last-child(4) {
       ....
  }

into the slightly more manageable:

  body > .layout > .body > .content 
    :matches(.post, .page) 
    :matches(p, li) 
    :matches(a.image.standard:first-child:nth-last-child(4), 
             a.image.standard:first-child:nth-last-child(4) ~ a.image.standard), 
       ....
  }

The Mozilla reference page above lists some caveats about performance. As this selector makes it out into a standard, we will hopefully see performance work on this to make it leaner.

:nth-child(An+B [of S])

While :nth-of-type has existed since the turn of the millennium, CSS Selectors Level 4 is adding the ability to filter based on a selector:

div :nth-child(2 of .widget)

The selector S is used for determining the index and it is independent of the selector to the left of the pseudo-class. As noted in the specification, if you know the type of the element ahead of time the :nth-of-type selector can be converted into :nth-child(... of S) like so:

img:nth-of-type(2) => :nth-child(2 of img)

The difference between this selector and the :nth-of-type selector is subtle but important. For :nth-of-type, each element –whether or not you have applied a selector to it – has an implicit index for itself amongst its siblings with the same tag name. The selector in the :nth-child(n of S) expression creates a new counter each time you use a new selector.

There’s potential for bugs with this new selector. Since the selector inside the :nth-child pseudo-class is independent of the selector to the left of it, you can accidentally omit your subject if you specify a selector to the left that isn’t a superset of the selector inside of :nth-child. For example:

tr:nth-child(2n of [disabled])

might not work as you expect if another, non-<tr> element has the disabled attribute.

In previous versions of the specification this was the :nth-match selector.

:not()

While you might have been using :not for some time, you’ll now be able to pass multiple arguments to it to save some bytes and typing:

// Equivalent to:
//    :not(h1):not(h2):not(h3)...
:not(h1, h2, h3, h4, h5, h6)

Descendant combinator (>>)

The descendant combinator has existed in CSS from the beginning as a space ( ), but now there’s an explicit version of it:

// Equivalent to:
//    p img { ... }
p >> img { ... }

The reasoning for this is to provide a bridge between the direct descendant (>), and the shadow DOM (>>>) operator.

Column combinator (||) and :nth-column

CSS Selectors 4 adds column operations that will allow stylesheet developers to more easily style individual columns in a table. The current approach to table styling requires using :nth-child, which does not always match up with table columns when using colspan attributes.

By using the new column combinator (||) you can now style table cells that are in the same column as a given <col> element:

// The following example makes cells C, E, and G yellow. 
// (example taken from the CSS Selectors 4 specification)
col.selected || td {
  background: yellow;
  color: white;
  font-weight: bold;
}

<table>
  <col span="2">
  <col class="selected">
  <tr><td>A <td>B <td>C
  <tr><td colspan="2">D <td>E
  <tr><td>F <td colspan="2">G
</table>

Alternatively, a stylesheet author may use :nth-column and :nth-last-column to style cells.

In either case, if a cell spans multiple columns it will match a selector for any of those columns.

:placeholder-shown

One small addition to the selector language is :placeholder-shown. This matches an input element if and only if the placeholder attribute text is visible.

:any-link

The :any-link is another small selector addition. It is defined as matching anything that either :link or :visited would match.

// Equivalent to:
//    a:link, a:visited { ... } 
a:any-link { ... }

Conclusions

CSS Selectors 4 is still a work-in-progress, but there are already useful selectors that we’ve seen that will offer web developers new patterns and tools for styling. There are other new selectors in the specification that I haven’t discussed here for concepts like accessibility, validity checking and style scoping.

If you’d like to play around with these selectors, you’ll need to wait for browsers vendors to catch up, or use some of the earlier implementations. :matches is available as :moz-any and :webkit-any, and WebKit nightlies have early support for :nth-child selectors behind a flag.

Since this is an editor’s draft, pseudo-class names may change without notice. Keep an eye on the specification for more information.

Comments? Follow me on Twitter @mmastrac and let me know.