grack.com

Blog

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.

(Ab)using CSS3's :nth-child selector to invent new ones

If you find this interesting, read more about CSS Selectors Level 4 which will offer you even more tools for stylesheet development.

CSS3’s :nth-child and :nth-last-child selectors are powerful: not only can they replace :first-child and :last-child, but they can style more complex patterns like the first (or all but the first) three children, every fourth child, or combinations of the pattern “a*n+b”.

But did you know that you can do more interesting things with the selectors? For example, you can style the third element, but only when it’s one of five child (the virtual :nth-of-m-child selector we’ll discuss below). Or that you can style all of the children of an element with m children (another virtual selector we’ll call :family-of-m).

You might ask why we’d want to do this – the particular use case that I had in mind was a Javascript-free, automatically-sizing image gallery that I could toss in the stylesheet for my Jekyll-based site and have it “just work” regardless of the number of images I threw at it.

Here’s an example of what it looks like (click here to view it full-page). Note how the images automatically size to a regular grid without having to use Javascript:

:nth-of-m-child

Thanks to xantys on HN for pointing me at much earlier work in this area here and here.

The first virtual selector we’ll construct is something I call :nth-of-m-child. This will allow us to style the nth child when it’s one of m children, and will be our building block for further work.

So, how do we get this selector? Easy: we combine :nth-child and :nth-last-child on a single element to select when the element to be styled is in the correct position from the start and end of the list of children. For example, we can style the third element, if and only if it’s one of five children:

span:nth-child(3):nth-last-child(3) { ... }

Breaking that down: that’s the third child and the third-last child. Given ‘n’ and ‘m’, the general formula is :nth-child(n):nth-last-child(m+1-n).

Here’s an example of this in action (click here to view it full-page):

:family-of-m

Now that we have the ability to style n-of-m, we can expand that out to style all of the children where there are m of them directly underneath a parent node. The secret to this is using the CSS3 ~ non-adjacent sibling selector which will continue matching elements across siblings. For example, the selector:

img ~ span { ... }

will match a <span> if and only if one of its previous siblings was an <img> element regardless of the number of siblings between them. We’ll combine this selector with our :nth-of-m-child pattern like so:

span:nth-child(1):nth-last-child(5) ~ span { ... }

This pattern will match any adjacent siblings to the first element of five, ie: the second through the fifth of five. We can make it match the entire row by using a comma to match the first element as well.

span:nth-child(1):nth-last-child(5), 
	span:nth-child(1):nth-last-child(5) ~ span { ... }

Here’s an example of this in action (click here to view it full-page):

Advanced techniques

Alright, now we have the tools in place for us to style images as seen in the example.

The first grouping of one through four are simple applications of the technique. When we get to five we want to start using a pattern where the first line or two are larger pairs and the remainder of the images are in triplets. This pattern should repeat regardless of the number of images.

Here’s a commented example. Note that this might potentially be simplified using flexbox and wrapping, but that’s an exercise for the reader.

Let’s start with fifth, eighth, eleventh and the remainder of this pattern. Instead of using the :nth-child(1):nth-last-child(...) as we did before, we’ll use :nth-child(1):nth-last-child(3n+5). This will match the first element of a grouping of 5, 8, 11, …:

/* First two are half-sized (99% / 2) */
img:first-child:nth-last-child(3n+5) ~ img, img:first-child:nth-last-child(3n+5) {
	max-width: 49.5%;
	margin-right: 1%;
}

/* Last n - 2 are (98% / 3) */
img:first-child:nth-last-child(3n+5) + img ~ img {
	max-width: 32.6%;
	margin-right: 1%;
}

/* But second, fifth, eighth, ... have no right margin */
img:first-child:nth-last-child(3n+5) ~ img:nth-child(3n+2) {
	margin-right: 0;
}

Six, nine, and twelve are much simpler.

/* Six, nine, twelve, ... are all (98% / 3) */
img:first-child:nth-last-child(3n+6) ~ img, img:first-child:nth-last-child(3n+6) {
	max-width: 32.6%;
	margin-right: 1%;
}

/* Every third one of these has no right margin. */
img:first-child:nth-last-child(3n+6) ~ img:nth-child(3n) {
	margin-right: 0;
}

Seven, ten, thirteen, and the remainder of the pattern are similar to the 5/8/11 pattern:

/* First four are half-sized (99% / 2) */
img:first-child:nth-last-child(3n+7) ~ img, img:first-child:nth-last-child(3n+7) {
	max-width: 49.5%;
	margin-right: 1%;
}

/* Last n - 4 are (98% / 3) */
img:first-child:nth-last-child(3n+7) + img + img + img ~ img {
	max-width: 32.6%;
	margin-right: 1%;
}

/* The second and fourth, seventh, tenth, ... have no right margin */
img:first-child:nth-last-child(3n+7) + img, 
	img:first-child:nth-last-child(3n+7) ~ img:nth-child(3n+4) {
	margin-right: 0;
	outline: 1px solid red;
}

Conclusions, notes, and further work

We can do some really interesting things making using of :nth-child and sibling selectors. The techniques in this post will also work for the related selectors :nth-of-type and :nth-last-of-type.

Browser performance doesn’t appear to be noticeably affected on mobile or desktop using this technique. If you plan on scaling this up it’s obviously something you’ll want to test.

If you were to combine this with flexbox and flex-wrap you might be able to simplify the example even further and might be able to handle images of different sizes more elegantly.

You also can also use this to create other interesting patterns, like matching only when the total number of children is even or odd, or other factor-based qualifiers.

I’d love to hear of any ideas or improvements you might have. Play around with the JSFiddle here.

Thanks to Webucator for creating a video for this post as part of their CSS3 training course series:

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