IoT or “smart home” devices are probably the biggest current buzzword aside of “blockchain”. But why buy smart devices if you can turn your existing device into an IoT version. We decided to do exactly that and build an internet-enabled coffee machine using JavaScript and revive the Hyper Text Coffee Pot Control Protocol for its 19th birthday.

Comic strip from Geek & Poke about their coffee machine unfollowing them on Twitter

Story Time

My flatmate and I already have a few internet-enabled devices in our apartment that we can control using our Amazon Echo Dots. It’s definitely convenient (read lazy). But the thing that is really missing is being able to trigger the coffee machine right from the bed.

Luckily my flatmate had a DeLonghi Latissima standing around that he wasn’t too emotionally attached to and that had electronic buttons which made this whole system seem hackable.

But why would we use JavaScript? Well my flatmate isn’t a developer and I haven’t written a single line of C++ in 3 years. JavaScript on the other hand is a language that I’m quite comfortable with. Hardware and JavaScript also both have a very event-driven focus, both center around events like “was this button pressed?”. Additionally, it seemed like one more challenge since there is definitely less documentation about hardware-hacking with JavaScript.

The Hardware Options

I was planning to do a hardware hack with JavaScript for a while and I had a couple of different options flying around my apartment already that we all considered for the hack:

Espruino

Espruino Logo

Espruino is an open source firmware that lets you run JavaScript on low power microcontrollers such as the ESP8266 or one of their own boards.

Espruino comes with a Web-based IDE that allows you to easily write JavaScript and deploy it to your hardware. It also provides a variety of modules with module system similar to the Node.js one. However, it is not Node.js that is running on the microcontrollers.

The only Espruino board that I had at home was an Espruino Pico which also doesn’t contain WiFi. I could have soldered an ESP8266 onto it for WiFi but who got time for that? We want coffee!

Tessel

Logo of Tessel

The next alternative is another open source hardware project called Tessel. Part of the Tessel project is the Tessel 2. A piece of hardware that can not only be controlled with JavaScript but it can run full Node.js and it can store the whole program on the hardware so we don’t have to keep a computer connected to it.

Photo of a Tessel 2 board

Johnny-Five

Logo of Johnny Five

The third alternative was to use Johnny-Five. It is actually an npm module that can talk to a variety of microcontrollers. Initially it uses the Firmata protocol to talk to Arduinos, but there are also a variety of other platforms that can be controlled with Johnny-Five via I/O plug-ins. This way we could for example use a Particle Photon, a Raspberry PI or even the Tessel 2.

Our Choice

We decided to go for the combination of Tessel and Johnny-Five. This way we can write code that is fairly platform agnostic and yet have code that can be run directly on the hardware without having to have a computer connected to it or run some code in the cloud.

Trust Me I’m An Engineer

Alright now that we knew what tools we would use it was time to start hacking! Since hardware doesn’t have a version control systems let’s have a last look at our beautiful and working coffee machine and store it in our memories:

Picture of original Latissima

Now let’s get going! We are both not electrical engineers and while I did study two semesters in university, I don’t remember anything.

Dog floating in space with caption I have no clue what I'm doing here

Important! We’ll be working with hardware that can get very hot and with electricity. Some of the stuff we are doing from this point on is neither smart or nor a safe approach. So if you do the same, you do it on your own risk.
Gif of Homer getting an electro shock

So how do you get started with hardware hacking when you have no clue?

Well we decided to do the most obvious thing and just take it apart and have a look! So bye bye screws!

Picture of opened Latissima

There is quite some stuff to see here. But what is of interest? I mean we don’t want to change the way the coffee is brewed but just trigger the brewing process and turn the coffee machine on/off.

Luckily this seems doable. Because if we look closely we can see that the control plate that holds the buttons and the microcontroller that controls the brewing process are connected via a cable a normal pluggable cable.

Annotated Picture of opened Latissima highlighting Buttons and Microcontroller

So if we can emulate the signal that is sent between the control plate and the microprocessor we would be able to control the coffee machine. Now we only need to figure out what the signal is.

What Are We Doing Here?

To figure out what the signal is that is being sent around, let’s first take a look at the control plate:

Picture of control plate with buttons and LEDs

We can see the following interesting components on it:

  • a plug with 8 pins
  • 7 LEDs
  • 6 button switches

Our theory is that there must be at least one pin of those eight that has to provide power. Since the cable has one of the eight wires marked red we figured that this might be power. So what do the other 7 pins do? If we think like a programmer we could use a binary flag to represent the states of the 7 LEDs and 6 switches, we would need to use 3 pins for the LEDs and 3 for the switches. What is the last pin doing though?

Based on our theory that the first pin is power, let’s connect some power to it and see what happens to the other pins when we press them. For that we connected the one end of the original wire to the control plate and jammed a bunch of jumper wires into the other end. This way we can connect the 7 unknown pins to our Tessel pins.

Picture of cable with jumper wires in it

For power we can choose from a 3.3V and a 5V connection. We tried connecting 3.3V first and saw that the LEDs barely lit up. So we tried 5V (very scientific work going on here) and it worked! Well it semi-worked. Three of our 7 LEDs (LED4, LED5, LED6) lit up. It’s still progress!

The Tessel comes with analog and digital pins. This means we can measure voltage at those pins just as well as a digital signal (high/low aka 0 and 1). Our first attempt was to measure the voltage on the remaining pins.

We wrote a small script for our Tessel that would initialize the connection to the board using Johnny-Five and creates pin instances in analog mode (mode: 2). It would then inject these pins into the Johnny-Five REPL so that we could measure their values whenever we wanted.

analogread.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const five = require('johnny-five');
const Tessel = require('tessel-io');
const board = new five.Board({
io: new Tessel()
});
board.on('ready', function () {
const p2 = new five.Pin({
pin: 'b1',
mode: 2
});
const p3 = new five.Pin({
pin: 'b2',
mode: 2
});
const p4 = new five.Pin({
pin: 'b3',
mode: 2
});
const p5 = new five.Pin({
pin: 'b4',
mode: 2
});
const p6 = new five.Pin({
pin: 'b5',
mode: 2
});
const p7 = new five.Pin({
pin: 'b6',
mode: 2
});
const p8 = new five.Pin({
pin: 'b7',
mode: 2
});
const pins = [p2, p3, p4, p5, p6, p7, p8];
board.repl.inject({ pins: pins });
});

Unfortunately, the analog reading wasn’t getting us where we wanted. The measurements were to fluctuating and therefore we couldn’t get any information out of it. So maybe digital readings would help us better:

analogread.js
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const p2 = new five.Pin({
pin: 'b1'/*,
mode: 2 */
});
const p3 = new five.Pin({
pin: 'b2'/*,
mode: 2 */
});
const p4 = new five.Pin({
pin: 'b3'/*,
mode: 2 */
});
const p5 = new five.Pin({
pin: 'b4'/*,
mode: 2 */
});
const p6 = new five.Pin({
pin: 'b5'/*,
mode: 2 */
});
const p7 = new five.Pin({
pin: 'b6'/*,
mode: 2 */
});
const p8 = new five.Pin({
pin: 'b7'/*,
mode: 2 */
});

With this setup we were playing around for a bit to see if we can control the LEDs or figure something else out. And we did! After trying to set the digital pins to HIGH and LOW respectively we figured out that Pin 4-6 were able to turn the LEDs LED4, LED5 and LED7 on and off. We weren’t able to turn the other LEDs on though. But at least we knew these were connected to the LEDs. So we knew 4 pins. What’s with the others?

Gif of Homer confusingly sorting through stuff

Maybe they are buttons! We initialized the other pins as buttons and tried if that worked:

analogread.js
36
37
38
39
40
41
42
43
44
45
46
board.repl.inject({ pins: pins });
const btn2 = new five.Button('b1');
const btn3 = new five.Button('b2');
const btn7 = new five.Button('b6');
const btn8 = new five.Button('b7');
const buttons = [btn2, btn3, btn7, btn8];
buttons.forEach(btn => {
btn.on('press', () => console.log('Pressed button no.%d', btn.pin));
btn.on('release', () => console.log('Released button no.%d', btn.pin));
});
});

This is also highlighting one strength of JavaScript and Hardware. Every JavaScript developer will immediately feel at home here. We have a EventEmitter based class Button that has a 'release' and 'press' event that we can listen on.

And we measured something!!! If we pressed the top two switches we could detect btn7 and btn8 triggering their events, if we pressed the two bottom right ones also btn7 and btn8 trigger. And if we set p3 to HIGH we can detect the behavior as for the other two.

Picture of control plate with buttons and LEDs

So what do we know?

  • Pin 1 is ⚡️ (turns on LED4, LED5, LED7)
  • Pin 4-6 can turn off 💡 LED4, 💡 LED5, 💡 LED7
  • Pin 7-8 react on Switches (S1-S6)
  • Pin 2 can manipulate S3 && S4
  • Pin 3 can manipulate S1 && S2

Alright that’s something but not great. How are we supposed to figure out which button is pressed? It’s great that we can (de)activate them but we want to know when they are pressed. After 5h of digging in the dark it’s time for a strategy change!

Gif of monkey pushing down a laptop

Back To The Drawing Board

Logo of the Friting software

Let’s approach this in a more systematic way and try to understand what is happening in the circuit on the board. To do so we took the picture of the board, zoomed into it and traced the lines on it. We then verified an assumed connection between to points by measuring the resistance between the two points using a multimeter. If we found a connection we used a tool called Fritzing to mark up the connection in a diagram. Fritzing is a great open source tool to mark up hardware setups. On our diagram we skipped everything that seemed like a resistor. The result was this beautiful diagram:

Schema of circuit with LEDs and Switches

I know what you are thinking. This looks worse than before. But it wasn’t. The first thing that was easier for us to figure out was the LED connections. The important thing about LEDs to know is that they let current pass through in only one direction. Having them marked up in the diagram allowed us to follow the current and get a better understanding of the whole circuit.

Not only Pin 1 is power but also Pin 2 and 3 are power! That explains why the other LEDs weren’t lighting up. But what about the switches?

After 8 hours of slow progress and staring on this tiny board we realized that we missed one thing all along!

Control Plate with arrows pointing at the diodes

These three things weren’t resistors. They were diodes!!! Diodes allow just like LEDs (Light Emitting Diodes) for current to only flow in one direction. So we updated our diagram, removed the LEDs (since we solved that puzzle) and cleaned up the diagram.

Schema of circuit with Diodes and Switches

And we got it! We finally solved the secret of the mysterious protocol:

  • S1 = P3 ➡ P7
  • S2 = P3 ➡ P8
  • S3 = P2 ➡ P7
  • S4 = P2 ➡ P8
  • S5 = P1 ➡ P7
  • S6 = P1 ➡ P8
  • 💡 LED7 (Power lamp) = P1 ➡ P6
  • etc.

Did Someone Say Coffee?

How can we make coffee with this knowledge though? Since the switches dependent on the power that is being sent between two pins, faking the protocol entirely will be complicated. But there is an easier solution. We can use relays!

Relays are like switches just that we can control them digitally with our microcontroller and since we know the full circuit of the control plate now, we can build the same circuit (or a part) ourselves and simply replace the switches with relays.

We only had 4 relays and therefore we decided to go for the right three switches which control large coffee, espresso and power. Exactly what we needed! We also wired up the respective three LEDs to have visual feedback if everything is working.

Diagram of the breadboard setup for the Tessel

Our initial wire up obviously didn’t look as neat but more like this:

Picture of the first wire up of the Tessel

We didn’t have enough jumper wires so we had to plug our relay board up-side-down into the breadboard (so professional).

Now it was time to test our wire up. But before we turned on the coffee machine we had to write some code. We quickly threw together a small script that would allow us to manually control the three relays using the built-in Relay class of Johnny-Five and inject them into the REPL.

analogread.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const five = require('johnny-five');
const Tessel = require('tessel-io');
const board = new five.Board({
io: new Tessel()
});
board.on('ready', () => {
const espresso = new five.Relay({
pin: 'a4',
type: 'NO'
});
const grande = new five.Relay({
pin: 'a5',
type: 'NO'
});
const power = new five.Relay({
pin: 'a6',
type: 'NO'
});
espresso.close();
grande.close();
power.close();
board.repl.inject({
espresso: espresso,
grande: grande,
power: power
});
});

With the code in place it was time to pour some water into the coffee machine, turn on the coffee machine via its master switch on the back of the device and power up our code. If everything worked we should be able to turn on the device by running power.open(). It has to heat up for 20 seconds (until the LED stops blinking). Afterwards it’s time to brew coffee! Or well water for our first test case:

Gif of first run using the REPL

Yay it’s alive! It took 10 hours and the sun had started to rise but hey we got coffee!!! Or well we got hot water because we forgot to buy the capsules that we needed for the Nespresso but well it’s a first step.

Gif of cast of Friends jumping

The Internet of Coffee Pots

Great so we can brew coffee if my laptop is connected to the Tessel that is connected to the coffee machine and if I then write some code into the REPL. Now that’s not really the internet of coffee pots that we have here. Time to change that!

Luckily on this day 19 years ago there were some smart folks that developed the most life changing protocol in the world! The Hyper Text Coffee Pot Control Protocol or short HTCPCP for the purpose of being able to control internet-enabled coffee pots. If that isn’t a sign that we should revive this forgotten protocol in 2017 and use it to control our coffee machine.

What is HTCPCP?

So as you might have noticed HTCPCP is an April fools from 1998. The reason why it’s great is that it suggested as part of the RFC an HTTP status code 418 - I'm a teapot. This status code is up to today not an actual standard, yet a lot of software uses it as small easter eggs. Take Google for example: google.com/teapot.

But it suggests more than just this one status code so let’s look at a couple of things.

  1. It’s based on HTTP
  2. It suggests a BREW method aside of POST to be able to issue a brewing command
  3. It defines a coffee:// URI schema to be able to address different coffee pots
  4. It even internationalizes this URI schema. I highly suggest you reading that section. I’ll wait here.
  5. It suggests a new Accept-Additions header for you to be able to specify if you want some milk, whiskey or a pumpkin-spiced latte.
  6. It clearly defines the Content-Type and messages that can be passed.
  7. And much more.

In other words it’s destined to be implemented!

But before we get started we need to talk about some uncertainities and shortcomings of this RFC.

The document mentions twice two different Content-Type values that should be used. One is application/coffee-pot-command and one is message/coffeepot. Since we can’t use both we decided to to go for application/coffee-pot-command as the Content-Type that will be sent to the coffee machine and message/coffeepot is the response type.

Additionally, we have to deal with the fact that our coffee machine supports multiple types of coffees contrary to a standard coffee pot from 1997. Therefore we have to modify our URI schema to deal with that: coffee://host/pot-{identifier}/{coffee type}. This is important because the application/coffee-pot-command only supports start and stop as official values.

Let’s implement it!

418 - I'm a teapot

Gif of a happy teapot

Since HTCPCP is based on HTTP we figured that for convenience and integration reasons we will just implement a standard HTTP server using Node.js and the http module. In order to keep the code itself lightweight for the Tessel we decided to not use any library like Express and rather go with old school naive routing using the URL.

We also split up all the logic to interact with the hardware into it’s own file. This way we can easily swap it out for another coffee machine later and use the same server logic.

latissima.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
const EventEmittter = require('events');
const { Board, Relay, Pin } = require('johnny-five');
const Promise = require('bluebird');
const Tessel = require('tessel-io');
const PRESS_DURATION = 500;
class Latissima extends EventEmittter {
// Board
board = undefined;
// Relays
espressoRelay = undefined;
grandeRelay = undefined;
powerRelay = undefined;
// Others
// The coffee machine is automatically on when you flip the master switch
isOn = true;
// No this isn't a teapot
isTeapot = false;
// Available Additions
additions = [];
// Coffee Types
static Types = {
espresso: 1,
grande: 2
};
constructor(debug = false) {
super();
this.board = new Board({
io: new Tessel(),
repl: debug,
debug: debug
});
this.board.on('ready', () => {
this.initializePins();
this.emit('ready');
});
}
initializePins() {
this.espressoRelay = new Relay({
pin: 'a4',
type: 'NO'
});
this.espressoRelay.close();
this.grandeRelay = new Relay({
pin: 'a5',
type: 'NO'
});
this.grandeRelay.close();
this.powerRelay = new Relay({
pin: 'a6',
type: 'NO'
});
this.powerRelay.close();
}
pressPower() {
if (!this.isOn) {
// it needs to heat up for ~20 seconds
// TODO: Remove this when we can read if the machine is on
setTimeout(() => {
this.isOn = true;
}, 21 * 1000)
} else {
this.isOn = false;
}
return this.pressButton(this.powerRelay);
}
// Convinience function to press different coffee types
press(type) {
switch(type) {
case Latissima.Types.espresso:
return this.pressButton(this.espressoRelay);
case Latissima.Types.grande:
return this.pressButton(this.grandeRelay);
default:
return Promise.reject(new Error("Could not find Type"));
}
}
// Emulate the push of a button by opening
// the respective relay, wait for a certain
// time and closing it again
pressButton(relay) {
return new Promise((resolve, reject) => {
relay.open();
setTimeout(() => {
relay.close();
resolve();
}, PRESS_DURATION);
});
}
}
module.exports = { Latissima };

Some features of JavaScript that we used in here were not available in the Node version on the Tessel yet so we transpiled it to ES5 compatible code using TypeScript.

As for the server we decided to implement the following subset of features:

1. GET, POST and BREW

We implemented the acceptance of the BREW method as well as traditional GET to get the status of the coffee machine and POST to issue commands.

index.js
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
if (req.method === 'GET') {
console.log('GET');
res.setHeader('Safe', 'yes');
res.setHeader('Content-Type', 'message/coffeepot');
if (req.url.indexOf('/pot-0/') === 0) {
console.log('TYPE INFO');
// TODO: Implement status of coffee production
let data = [];
res.write(data.join('\n'));
} else if (req.url.indexOf('/pot-0') === 0) {
let data = ['isOn='+coffeeMachine.isOn]
res.write(data.join('\n'));
} else {
res.statusCode = 404;
}
return res.end();
}
if (req.method === 'POST' || req.method === 'BREW') {

2. Content-Type verification

We only accept application/coffee-pot-command and text/plain as valid content types for POST and BREW requests. We had to add text/plain for our later integration with Alexa.

index.js
147
148
149
150
function hasCorrectContentType(req) {
return req.headers['content-type'] === 'application/coffee-pot-command'
|| req.headers['content-type'] === 'text/plain';
}

3. Initial Accept-Additions support

With just four relays we didn’t have enough to control the full functionality of the coffee machine and therefore our coffee machine doesn’t support any additions. However, we did add verification of the header and the correct behavior when someone orders an addition that our coffee machine doesn’t support. Which, at the moment, are all additions.

index.js
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
function getAdditionsRequested(req) {
let header = req.headers['accept-additions'];
let milkType = ['Cream', 'Half-and-half', 'Whole-milk', 'Part-Skim', 'Skim', 'Non-Dairy'];
let syrupType = ['Vanilla', 'Almond', 'Raspberry', 'Chocolate'];
let alcoholType = ['Whisky', 'Rum', 'Kahlua', 'Aquavit'];
let spiceType = []; /** NO LIST DEFINIED IN RFC */
let validTypes = [
'*',
...milkType,
...syrupType,
...alcoholType,
...spiceType
];
if (!header) {
return [];
}
return header.split(';')
.map(type => type.trim())
.filter(type => validTypes.indexOf(type) !== -1);
}

4. 418 - I'm a teapot

Well obviously, we had to implement this trivial check that simply checks if the coffee pot that is connected to our server is in fact a disguised tea pot. In which case we return the appropriate status code.

index.js
32
33
34
35
36
37
38
function handleRequests(req, res) {
if (coffeeMachine.isTeapot) {
res.statusCode = 418;
res.statusMessage = "I'm a teapot";
res.write("I'm a teapot");
return res.end();
}

What we didn’t implement:

We are currently unable to programatically check for the state of the coffee machine. The LEDs are showing the respective state but we have to solve the problem of being able to detect this state inside the Tessel 2. Otherwise, we are not able to properly implement the Safe header or the GET status.

While we did implement a basic version of the coffee:// schema we didn’t implement the full thing and had to alter it. Else, we would have had a way harder time using this with other platforms such as Amazon Alexa.

In order to make the Tessel easily addressable we installed additionally the localtunnel module to create an externally addressable URL.

And it works! 🎉 You can see a working version of the HTCPCP protocol here:

The full code is also available on GitHub.

IFTTC - If This Then Coffee

Now that we had a working HTTP server that we could use to control our coffee machine it was time to connect it to Alexa. The optimal solution here would be to build a Smart Home skill to be able to use it in combination with the other smart home devices in the household. However, time is valuable so let’s try a quicker approach: IFTTT (If-This-Then-That).

Luckily, Alexa allows you to build simple skills using IFTTT and IFTTT allows you to make HTTP POST requests. That’s all we need to control our coffee machine!

Example Alexa IFTTT Skill

We spun up three skills. One that turns on the coffee machine if it’s in stand-by, one that makes a normal coffee and one that makes espresso.

Screenshot of all Alexa IFTTT Skills

The great thing was that it was easy as it sounds. In just 3 minutes we had our Alexa integration done. But see for yourself:

What’s Next?

We are still super excited that we got this initial version but obviously there is so much work left of things we have to, should and can do. Some of the things on our agenda are:

  • Figuring out how we can determine the state of the coffee machine programatically
  • Add more relays to have the full functionality ready
  • Use some sensor to determine if a cup is under the coffee machine
  • Figure out how we can make Irish coffee aka. how do we get Whiskey in the coffee
  • Make a casing for the whole setup to be neatly placed on top of the coffee machine
  • Add a Twilio integration so that we can trigger the coffee brewing even remotely without Alexa
  • Find a solution for the problem that we manually need to place a new Nespresso cap into the device

Basically every day this list is getting longer because we have some new cool idea and I’m certain you might have an idea, too, what we should do. If you do, make sure to send me a tweet @dkundel. We would love to hear your ideas!

Conclusion

It was great doing this little side project. Our modern devices are so closed up that we rarely anymore open devices to check up what’s going on on the inside. I must also say that working with the Tessel 2, Johnny-Five and JavaScript was amazing. It’s a great way to get started in hardware hacking. I personally have a Johnny-Five Inventors Kit from Sparkfun at home and I can’t wait to find some time and play around with other pieces in the kit for more projects.

I think JavaScript’s event-oriented approach is a perfect match for hardware. Sure if you want to build the most energy and space efficient project you should resort back to writing C++ but for hacking projects like this it’s a great solution.

All in all, this was a fun project and it was great to combine finally exploring hardware hacking with reading and implementing the HTCPCP RFC. I hope we were able to inspire you to explore the field of hardware hacking and if you do something make sure to let us know on Twitter! Maybe somone of you will hack their toaster next 😉

Gif of toaster