Connecting an Apple Cinema Display to a 2017+ MacBook Pro with Thunderbolt 3

When I started my new job, I was provided a new MacBook Pro to use as my work computer. I wanted to fit my new MacBook into my existing 27” Apple Cinema Display from 2010, which I’ve been using with my personal 2013 MacBook Air for 5 years. Should be easy, right? They’re both Apple products?


It turns out that Apple doesn’t even offer a first-party solution to connect the new Thunderbolt 3 (USB-C shaped port) MacBook Pro to the Cinema Display.

I went through 4 different orders on Amazon before I found a third-party combination that works, but now it’s working great.


The requirements

The Apple Cinema Display provides 3 ports in its cable - MagSafe power delivery; USB-A for the included USB hub, webcam, and speakers; and Mini DisplayPort for video.


To get all three functions on my MacBook Pro, I needed:

  1. A MagSafe to USB-C adapter for power delivery
  2. A Thunderbolt 3 to Mini DisplayPort for video
  3. A USB-C to USB-A adapter for everything else.

Unfortunately the 13” MacBook Pro only has 2 Thunderbolt 3 ports, so I also needed some way to daisy chain or collapse two of these functions into one port.

The solution

I eventually found the right combination of adapters:

UPTab USB-C Type C to Mini DisplayPort Adapter 4K@60Hz

This has the important feature of the extra pass-through power port. null

Syntech USB C to USB Adapter [2-Pack], Thunderbolt 3 to USB 3.0 Adapter

A slim profile to adapt USB-C to USB-A. null

ELECJET Anywatt, USB C Magsafe Adapter, Type C

My only complaint about this adapter is that it gets very hot. null

In the process I learned a lot about the difference between Thunderbolt 3 & USB-C, which use the same connector but don’t provide the same functionality.

In the end, I figured it out - hopefully this will help you. A not-terribly-messy-looking, serviceable way to connect at 2010 27” Apple Cinema Display to a modern MacBook Pro with Thunderbolt 3.

Products I Love

We are in the middle of a “stuff” revolution.

The success of Mario Kondo’s book The Life-Changing Magic of Tidying Up and its Netflix successor Tidying Up speaks to what earlier books like Stuffocation have realized about our era: most of us have too much crap.

I fit neatly into the generational shift towards preferring experiences over things, but I’m also a hobbyist with a short attention span. Any time I take on a new hobby, I accumulate the associated stuff. And very occasionally, that stuff doesn’t make me feel “stuffocated” - it sparks joy, if you will.

These are a few of the things that “spark joy” for me. Hopefully they’ll bring you joy too.

Tovolo Narrow-Handled Jar Spatula

Flex-Core Stainless Steel Removable Head, Dishwasher Safe

Tovolo Spatula

I find it easiest to explain why I love this spatula so much by considering the variety of tasks that we put a spatula through: it’s a tool that needs to be flexible enough to conform to the edges of a container, but sturdy enough to scrape away stuck-on bits. It needs to be wide enough to flip an omelet but narrow enough to fit into a jam jar. It should never scratch the coating of a pan, but never get melted from the heat. It should be able to get tossed in the dishwasher but not leave food residue trapped inside it.

This spatula nails all of these, which alone make it the best spatula I’ve ever owned, but that’s not what sparks joy about it.

It’s the heavy stainless-steel handle.

Like a good knife that is balanced to require only the tiniest effort from a cook, this spatula considers the ergonomic of its use. The heft of the handle helps press the spatula into the bowl so you don’t have to use a ton of force to scrape the sides. It can rest on the edge of a cutting board without tipping over the edge of the counter. And it’s removable, so you can thoroughly wash the joint between the head and the handle.

You can get one here, and help a charity through Amazon Smile:

Buttermilk Co - Fresh Indian Food in 5 Minutes

Buttermilk Co Food

I never had Indian food until I was in college. I loved the richness and flavor-forwardness of the food in Indian restaurants in the U.S., and as I sought out more I learned there’s a whole world beyond what you get in those restaurants. Indian food has featured heavily in my Vegan January cooking explorations, and is an extremely versatile cuisine for the Instant Pot that I was recently gifted (for excellent Instant Pot Indian recipes, check out My Heart Beets). Buttermilk brings those real flavors of Indian kitchens, not just what’s found in restaurants, right to your freezer. The food is packaged fresh but freezes well, is all vegan, and takes around 5 minutes to heat up in a microwave. Their upma has become my weekday breakfast staple. Get some Buttermilk and help me get some more too at

MROCO Large Waterproof Mousepad Mouse Mat (14” x 11”)

Mouse mat

For years, I have used the surface of a desk or the bottom-tier mousepads freely distributed at my office. Spending $7 on a large mouse pat with a proper grip has improved my computer experience more than I ever expected. My mouse glides over the surface, it feels like an air-hockey table. This keeps up with the spatula’s theme of a tool helping me expend as little effort as necessary to use it well. It’s pretty new to me, so we’ll see how well it holds up, but I expect the stitched edges to go a very long way in preventing fraying. Get one at

Macpac Merino 150 Boxer Briefs

Macpac boxers

I bought these Merino wool boxer briefs while traveling in New Zealand, and they’re still my favorite pair after a year of wear. Merino is naturally anti-odor and moisture-wicking. These don’t ride up, the elastic in the waist hasn’t stretched out, and I can wear them for multiple days of a backpacking trip (ew) and they’re still comfortable and far less ripe than a dri-fit T-shirt worn at the same time (I know from experience). You can’t get them shipped to the U.S., but maybe ask a friend down under to mule them home for you? (Looking at you, Zac).

Onsen Bath Sheet Towel

Onsen bath towel

These towels are outrageously expensive but totally worth it. They are woven with these little waffle pockets that soak up water but are very breathable, so the towel always feels cool and light and always has capacity to dry more.

Is there something you love that I should know about? Drop me a line @z3ugma.

An Introduction to M: The NoSQL Database with a Programming Language

I originally wrote this for a tutorial on, which attempts to teach you the basics of a new programming language. I use M daily at work, and it gets a lot of unfair criticism in tech journalism for its age and its terseness. I liken M to Python in its usefulness for procedural scripting. It’s also got a very nice debugging tool that lets you pause code mid-execution and inspect the variables as you’re running it. Finally, the database is built right into the language. While I think that setting up a new M environment can be tedious and require a lot of Unix knowledge compared to other programming environments, database replication is very simple compared to some other databases.

Without further ado, here’s an introduction to M:

M, or MUMPS (Massachusetts General Hospital Utility Multi-Programming System) is a procedural language with a built-in NoSQL database. Or, it’s a database with an integrated language optimized for accessing and manipulating that database. A key feature of M is that accessing local variables in memory and persistent storage use the same basic syntax, so there’s no separate query language to remember. This makes it fast to program with, especially for beginners. M’s syntax was designed to be concise in an era where computer memory was expensive and limited. This concise style means that a lot more fits on one screen without scrolling.

The M database is a hierarchical key-value store designed for high-throughput transaction processing. The database is organized into tree structures called “globals”, which are sparse data structures with parallels to modern formats like JSON.

Originally designed in 1966 for the healthcare applications, M continues to be used widely by healthcare systems and financial institutions for high-throughput real-time applications.


Here’s an example M program to calculate the Fibonacci series:

fib ; compute the first few Fibonacci terms
    new i,a,b,sum
    set (a,b)=1 ; Initial conditions
    for i=1:1 do  quit:sum>1000
    . set sum=a+b
    . write !,sum
    . set a=b,b=sum


;   Comments start with a semicolon (;)

Data Types

M has two data types:

;   Numbers - no commas, leading and trailing 0 removed.
;       Scientific notation with 'E'.
;       Floats with IEEE 754 double-precision values (15 digits of precision)
;       Examples: 20, 1e3 (stored as 1000), 0500.20 (stored as 500.2)
;   Strings - Characters enclosed in double quotes.
;       "" is the null string. Use "" within a string for "
;       Examples: "hello", "Scrooge said, ""Bah, Humbug!"""


Commands are case insensitive, and have a shortened abbreviation, often the first letter. Commands have zero or more arguments,depending on the command. M is whitespace-aware. Spaces are treated as a delimiter between commands and arguments. Each command is separated from its arguments by 1 space. Commands with zero arguments are followed by 2 spaces.


Print data to the current device.

WRITE !,"hello world" 

! is syntax for a new line. Multiple statements can be provided as additional arguments:

w !,"foo bar"," ","baz" 


Retrieve input from the user

READ var
r !,"Wherefore art thou Romeo? ",why

Multiple arguments can be passed to a read command. Constants are outputted. Variables are retrieved from the user. The terminal waits for the user to enter the first variable before displaying the second prompt.

r !,"Better one, or two? ",lorem," Better two, or three? ",ipsum


Assign a value to a variable

SET name="Benjamin Franklin"
s centi=0.01,micro=10E-6
w !,centi,!,micro



Remove a variable from memory or remove a database entry from disk.

KILL centi
k micro

Globals and Arrays

In addition to local variables, M has persistent variables stored to disk called globals. Global names must start with a caret (^). Globals are the built-in database of M.

Any variable can be an array with the assignment of a subscript. Arrays are sparse and do not have a predefined size. Arrays should be visualized like trees, where subscripts are branches and assigned values are leaves. Not all nodes in an array need to have a value.

s ^cars=20
s ^cars("Tesla",1,"Name")="Model 3"
s ^cars("Tesla",2,"Name")="Model X"
s ^cars("Tesla",2,"Doors")=5

w !,^cars 
; 20
w !,^cars("Tesla")
; null value - there's no value assigned to this node but it has children
w !,^cars("Tesla",1,"Name")
; Model X

Arrays are automatically sorted in order. Take advantage of the built-in sorting by setting your value of interest as the last child subscript of an array rather than its value.

; A log of temperatures by date and time
s ^TEMPS("11/12","0600",32)=""
s ^TEMPS("11/12","1030",48)=""
s ^TEMPS("11/12","1400",49)=""
s ^TEMPS("11/12","1700",43)=""


; Assignment:       =
; Unary:            +   Convert a string value into a numeric value.
; Arthmetic:
;                   +   addition
­;                   -   subtraction
;                   *   multiplication
;                   /   floating-point division
;                   \   integer division
;                   #   modulo
;                   **  exponentiation
; Logical:  
;                   &   and
;                   !   or
;                   '   not
; Comparison:
;                   =   equal 
;                   '=  not equal
;                   >   greater than
;                   <   less than
;                   '>  not greater / less than or equal to
;                   '<  not less / greater than or equal to
; String operators:
;                   _   concatenate
;                   [   contains ­          a contains b 
;                   ]]  sorts after  ­      a comes after b
;                   '[  does not contain
;                   ']] does not sort after

Order of operations

Operations in M are strictly evaluated left to right. No operator has precedence over any other. You should use parentheses to group expressions.

w 5+3*20
;You probably wanted 65
w 5+(3*20) 

Flow Control, Blocks, & Code Structure

A single M file is called a routine. Within a given routine, you can break your code up into smaller chunks with tags. The tag starts in column 1 and the commands pertaining to that tag are indented.

A tag can accept parameters and return a value, this is a function. A function is called with ‘$$’:

; Execute the 'tag' function, which has two parameters, and write the result.
w !,$$tag^routine(a,b) 

M has an execution stack. When all levels of the stack have returned, the program ends. Levels are added to the stack with do commands and removed with quit commands.


With an argument: execute a block of code & add a level to the stack.

d ^routine    ;run a routine from the begining. 
;             ;routines are identified by a caret.
d tag         ;run a tag in the current routine
d tag^routine ;run a tag in different routine

Argumentless do: used to create blocks of code. The block is indented with a period for each level of the block:

set a=1
if a=1 do  
. write !,a
. read b
. if b > 10 d
. . w !, b 
w "hello"


Stop executing this block and return to the previous stack level. Quit can return a value.


Clear a given variable’s value for just this stack level. Useful for preventing side effects.

Putting all this together, we can create a full example of an M routine:

; RECTANGLE - a routine to deal with rectangle math
    q ; quit if a specific tag is not called

    n length,width ; New length and width so any previous value doesn't persist
    w !,"Welcome to RECTANGLE. Enter the dimensions of your rectangle."
    r !,"Length? ",length,!,"Width? ",width
    d area(length,width)            ;Do a tag
    s per=$$perimeter(length,width) ;Get the value of a function
    w !,"Perimeter: ",per

area(length,width)  ; This is a tag that accepts parameters. 
                    ; It's not a function since it quits with no value.
    w !, "Area: ",length*width
    q ; Quit: return to the previous level of the stack.

    q 2*(length+width) ; Quits with a value; thus a function

Conditionals, Looping and $Order()

F(or) loops can follow a few different patterns:

;Finite loop with counter
;f var=start:increment:stop

f i=0:5:25 w i," " ;0 5 10 15 20 25 

; Infinite loop with counter
; The counter will keep incrementing forever. Use a conditional with Quit to get out of the loop.
;f var=start:increment 

f j=1:1 w j," " i j>1E3 q ; Print 1-1000 separated by a space

;Argumentless for - infinite loop. Use a conditional with Quit.
;   Also read as "forever" - f or for followed by two spaces.
s var=""
f  s var=var_"%" w !,var i var="%%%%%%%%%%" q  
; %
; %%
; %%%
; %%%%
; %%%%%
; %%%%%%
; %%%%%%%
; %%%%%%%%
; %%%%%%%%%
; %%%%%%%%%%

I(f), E(lse), Postconditionals

M has an if/else construct for conditional evaluation, but any command can be conditionally executed without an extra if statement using a postconditional. This is a condition that occurs immediately after the command, separated with a colon (:).

; Conditional using traditional if/else
r "Enter a number: ",num
i num>100 w !,"huge"
e i num>10 w !,"big"
e w !,"small"

; Postconditionals are especially useful in a for loop.
; This is the dominant for loop construct:
;   a 'for' statement
;   that tests for a 'quit' condition with a postconditional
;   then 'do'es an indented block for each iteration

s var=""
f  s var=var_"%" q:var="%%%%%%%%%%" d  ;Read as "Quit if var equals "%%%%%%%%%%"
. w !,var

;Bonus points - the $L(ength) built-in function makes this even terser

s var=""
f  s var=var_"%" q:$L(var)>10  d  ;
. w !,var

Array Looping - $Order

As we saw in the previous example, M has built-in functions called with a single $, compared to user-defined functions called with $$. These functions have shortened abbreviations, like commands. One of the most useful is $Order() / $O(). When given an array subscript, $O returns the next subscript in that array. When it reaches the last subscript, it returns “”.

;Let's call back to our ^TEMPS global from earlier:
; A log of temperatures by date and time
s ^TEMPS("11/12","0600",32)=""
s ^TEMPS("11/12","0600",48)=""
s ^TEMPS("11/12","1400",49)=""
s ^TEMPS("11/12","1700",43)=""
; Some more
s ^TEMPS("11/16","0300",27)=""
s ^TEMPS("11/16","1130",32)=""
s ^TEMPS("11/16","1300",47)=""

;Here's a loop to print out all the dates we have temperatures for:
n date,time ; Initialize these variables with ""

; This line reads: forever; set date as the next date in ^TEMPS.
; If date was set to "", it means we're at the end, so quit.
; Do the block below
f  s date=$ORDER(^TEMPS(date)) q:date="" d
. w !,date

; Add in times too:
f  s date=$ORDER(^TEMPS(date)) q:date=""  d
. w !,"Date: ",date
. f  s time=$O(^TEMPS(date,time)) q:time=""  d
. . w !,"Time: ",time

; Build an index that sorts first by temperature - 
; what dates and times had a given temperature?
n date,time,temp
f  s date=$ORDER(^TEMPS(date)) q:date=""  d
. f  s time=$O(^TEMPS(date,time)) q:time=""  d
. . f  s temp=$O(^TEMPS(date,time,temp)) q:temp=""  d
. . . s ^TEMPINDEX(temp,date,time)=""

;This will produce a global like

Further Reading

There’s lots more to learn about M. A great short tutorial comes from the University of Northern Iowa and Professor Kevin O’Kane’s Introduction to the MUMPS Language presentation.

To install an M interpreter / database on your computer, try a YottaDB Docker image.

YottaDB and its precursor, GT.M, have thorough documentation on all the language features including database transactions, locking, and replication:

WasherBot and DryerBot, Part 5: A Measurement Algorithm and Text Message Integration

Table of Contents

As we saw in Part 4, the “thicker” the AC wave appears on a graph of our MCP3002 measurements, the more power it’s consuming. This “thickness” is a great proxy for power consumption called the “amplitude” of the wave - the distance between the highest and lowest points of the wave.

When an appliance is using no power, the amplitude should be 0. In testing, I found that jittery power can sometimes produce a resting amplitude of up to +/-6, so our actual resting amplitude turns out to be 12.

In order to find the amplitude of the 60Hz wave, we need to measure it for at least 1/60th of a second. This will guarantee that we’ve measured an entire cycle of the wave. If we run the MCP3002 at 50 kHz, then we’ll need at least 50,000/60 = 834 measurements to measure a full cycle.

This will only let us measure a 1/60th of a second time period, though. Maybe the appliance isn’t using much power during that tiny window. If we increase the amount of time we’re measuring for, we can get a better assessment of power usage.

Let’s arbitrarily pick 7500 measurements. That’s just about 9 cycles, or 3/20ths of a second.

We can see that measuring for just 3/20ths of a second will accumulate a lot of data. If we want to do meaningful work with this long list of numbers, we need to simplify it down to a single representation of state.

State is a concept from computer science. When we say something has a state, we are describing the properties of that object at a particular point in time. Like on October 27th, a pumpkin might have a state of ‘picked’. Then, on October 30th, it goes from ‘picked’ to ‘carved’ and on Halloween it goes to ‘jack-o-lantern’. By November 3rd, it’s ‘rotten’.

Our pumpkin has many states, but our appliances really only have two for our purposes - ‘on’ and ‘off.’

Simplyifing the Measurement

To make a shorter list of numbers, we can take out unnecessary information from our sine wave. The ‘unnecessary’ information is basically ‘anything that isn’t a peak or a trough’ since those are the only points we care about to calculate amplitude.

We could just take the highest and lowest points of the entire 7500 item list and call it a day, but this might be a problem when there are outliers or measurement errors that are sudden spikes in the data. So we want to amplify the effect of peaks and troughs, but smooth out the effect of any one outlier peak or trough.

Make a sliding ‘window’ and every 100 measurements, find the max and min value of the 50 preceding and 50 next measurements. Taking the average of these ‘windowed’ amplitudes means that any one accidental spike will only count toward the average of its own peak or trough, and the rest of the wave cycles will push the average closer to its true value.

Notice how, in this image, a single spike makes the global maximum around 625 while the true maximum, seen over several cycles, is closer to 595. Using the ‘window’ approach pulls the average maximum closer to its true value.

To get from an average peak / trough to an average amplitude is as easy as subracting the trough from the peak.

Finally, we come to our measurement algorithm: a function that starts off by measuring each channel 7500 times at 50 kHz and spits out a single number - an adjusted-average amplitude.

def measure(number):    
    ch0list = []
    ch0avg = []
    for _ in range(1,number): #Measure n times


    for step in range(50,(number-50),100): # Make peak/trough windows

        ch0max = max(ch0list[step-50:step+50])
        ch0min = min(ch0list[step-50:step+50])

        ch0avg.append((ch0max - ch0min))

    ch0 = reduce(lambda x, y: x + y, ch0avg) / len(ch0avg) #Calculate the average amplitude

    return ch0

print measure(7500)

Let’s have a look at some real-life amplitude measurements during a washer and dryer cycle:

At rest, each appliance usually has an average-adjusted amplitude of 6.

From Amplitude to State, and Keeping Track of State

So then, to get from amplitude to appliance state is not a taxing calculation: Any amplitude greater than 12 is ‘on’, anything 12 or less is ‘off’

We set up our measurement script to run once every minute and find the current state of the appliance. But the script only knows what the appliance is doing this very minute. How can we find out what it did in the previous minute?

For that, we’ll need a database. InfluxDB is a great choice for this application, as it’s designed for measuring things over time.

So to add to our script that runs each minute, we’ll write the measurement and state to the database:

from influxdb import InfluxDBClient

USER = 'root'
PASSWORD = 'root'
DBNAME = 'db'
HOST = 'hostname'
PORT = 8086



mm = measure(7500) # As defined above

statemin = 12
state = 1 if (mm>statemin) else 0

newpoint = [{
    "measurement": "voltage",
    "fields": {
        "washer": mm
    "measurement": "state",
    "fields": {
        "washer": state

We can now keep track directly of when an appliance is on or off:

Comparing States and Taking Actions

Now we can compare the current state to the previous state.

Most of the time, we’ll find that the state is unchanged. An appliance in motion tends to remain in motion, and an appliance at rest tends to remain at rest. But occasionally, we’ll find that the state is different - the appliance started up or the appliance powered down.

So we have to get the last known state:

query = "SELECT * from state GROUP BY * ORDER BY DESC LIMIT 1"
result = client.query(query)
resultlist = list(result.get_points())
lastknownstate = resultlist[0]['washer']

Now we compare the last known state to the current state:

different = 0
currentstate = 1 if (measurement>statemin) else 0

if currentstate != lastknownstate:
    different = 1
    #Do stuff

There are two possible state transitions: ‘on’ to ‘off’ and ‘off’ to ‘on’. We’ve called these ‘powering up’ or ‘shutting down’. I only want to get text messages when an appliance shuts down - after all, when it turns on someone is standing there pushing the ‘on’ button.

To get text messages from our Python script we’ll use the awesome Twilio Python REST API.

import datetime
from import TwilioRestClient
import pytz
from datetime import datetime

account_sid = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Your Account SID from
auth_token  = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"  # Your Auth Token from
twilioclient = TwilioRestClient(account_sid, auth_token)

numbers = ["+15555555555", "+15555554444"] # Your intended SMS recipients

currenttime ='US/Central')).strftime('%-I:%m %p') # The time like '6:10 PM'
if (currentstate != lastknownstate) and currentstate == 0: #Only do this when the appliance turns off
            for n in numbers:
                twilioclient.messages.create(body=("💧 Washer is done! " + currenttime),
                    from_="+15555553333") # Replace with your Twilio number

Now I’m happily enjoying my laziness and a prompt turnaround on laundry:

WasherBot and DryerBot, Part 4: A Low-Pass Filter and Reading From Laundry Appliances

Table of Contents

When I was following the OpenEnergyMon guide I noticed they were including a capacitor in their circuit:

What’s that there for? It’s to provide an alternative path for some of the current to flow to the ground. Capacitors take some time to charge up and then discharge all at once, in a way that is dependent on the frequency of the current flowing through them. An arrangement like this with a resistor and a capacitor together is called a low-pass filter, meaning that only signals with a low frequency will be able to pass. It will filter out excess noise.

This is another great discussion on the subject.

Let’s hook up a 10uF capacitor to our circuit:

I did some experimenting by measuring the voltage signal with and without the low-pass filter.

Across the entire range of the ADC:

and zoomed in:

As you can see, it’s not trivial. A perfectly clean signal would measure 511 every time, but there’s some jitter. With the low-pass filter, the difference between the maximum value and the minimum is around 15. Without the filter, it’s much as 50. To a crude approximation, the filter adds around 10% accuracy to our measurement.

Two Jacks and Two Channels

Up to this point, we’ve only been measuring one input at a time. With 2 channels, the MCP3002 can read 2 voltages. And with 2 laundry appliances, that’s what I need to do.

We’ll add a second jack to the circuit:

We can share the same voltage divider, and direct our output to channel 1.

Measuring the Real Appliances

Now, in order to translate voltage readings into the state of the appliance, I need to actually measure the appliances. I turned the breaker off to the dryer, unplugged it, and took off the back cover to fit the current sensor around 1 of the wires. A 220V dryer in the US has 4 wires coming into it. The green ground, 2 phases of power (red and black) and a white neutral. After some trial and error, I found that my dryer had the heating element powered by the left phase and the motor powered by the right phase.

For the washer, I turned off the breaker to the outlet it was plugged into and put the current sensor around the power line inside the outlet.

I cued up the measurement script and took some readings of the dryer:

It was basically perfect. I was excited here - the washer reading stays almost exactly at baseline and the dryer reading starts out there. When the dryer turns on, it is instantaneously recognizable in the graph.

In Part 5, I’ll show you my algorithm for turning thousands of measurements into a single meaningful data point.