Random Musings of a Coffee Technologist
That feel when someone asks a question and the answer is, “I wrote the app for that.”
So here’s a reminder that BrewPlot exists. It’s really just a toy program that I wrote as a learning exercise, but if you have a scale and a way to measure the percent total dissolved solids in a coffee (a refractometer is the fastest and easiest way but the electrical conductivity meters also work) and don’t feel like doing the math yourself this plots extraction yield versus strength on a brewing control chart (golden cup guidelines helpfully drawn bold over the chart so it’s easy to see if you’re in the center box). One of the neat things about this is you can plot any number of brews, changing one of your brewing variables and snap a least squares fit line over the data to see how that variable affects your brew. Doing that is a good way to know how to modify your brewing if you find yourself out of spec one day.

That feel when someone asks a question and the answer is, “I wrote the app for that.”

So here’s a reminder that BrewPlot exists. It’s really just a toy program that I wrote as a learning exercise, but if you have a scale and a way to measure the percent total dissolved solids in a coffee (a refractometer is the fastest and easiest way but the electrical conductivity meters also work) and don’t feel like doing the math yourself this plots extraction yield versus strength on a brewing control chart (golden cup guidelines helpfully drawn bold over the chart so it’s easy to see if you’re in the center box). One of the neat things about this is you can plot any number of brews, changing one of your brewing variables and snap a least squares fit line over the data to see how that variable affects your brew. Doing that is a good way to know how to modify your brewing if you find yourself out of spec one day.

While the shop was closed on Monday I took the opportunity to play with the new coffee refractometer that I picked up at the SCAA conference in Houston to replace my trusty old (but requiring far too much sample preparation time to really be useful anymore) electrical conductivity TDS meter. I’ll be doing a series of similar tests as I have time and doing a proper writeup when everything is done, but today I did some analysis of the numbers, plugged them into BrewPlot, and produced this chart showing how strength and extraction change in relation to the prewet setting on my Fetco Extractor.
Brewing parameters: 0.34 pounds coffee (Sumatra Mandheling, all batches from the same roast), 3.25L water (6.44 pounds hot water if coffee isn’t brewed), 4 minute brewing time, 20 second prewet delay, 202 degree F water temperature.
As for flavor, I liked the taste of the 5% prewet best.

While the shop was closed on Monday I took the opportunity to play with the new coffee refractometer that I picked up at the SCAA conference in Houston to replace my trusty old (but requiring far too much sample preparation time to really be useful anymore) electrical conductivity TDS meter. I’ll be doing a series of similar tests as I have time and doing a proper writeup when everything is done, but today I did some analysis of the numbers, plugged them into BrewPlot, and produced this chart showing how strength and extraction change in relation to the prewet setting on my Fetco Extractor.

Brewing parameters: 0.34 pounds coffee (Sumatra Mandheling, all batches from the same roast), 3.25L water (6.44 pounds hot water if coffee isn’t brewed), 4 minute brewing time, 20 second prewet delay, 202 degree F water temperature.

As for flavor, I liked the taste of the 5% prewet best.

Inside BrewPlot

I recently released a little program called BrewPlot which allows a person to measure the mass of ground coffee used for brewing, the mass of the brewed coffee, and the percent total dissolved solids in the brewed coffee and see that plotted on a coffee brewing control chart. I did this mainly as a way to learn QML and how to have QML interact with C++. This post discusses a couple of the more interesting points in the program.

Creating Menus in QMainWindow from QML

When writing BrewPlot, there were certain functions that seemed natural to expose through a normal menu bar rather than writing a custom menu system in QML. I suspect that in most cases of programs written in both QML and C++ the menus would simply be set up in C++. I decided that I wanted to be able to define these menus from the QML side of things. To do this, I subclassed QMainWindow to add a Q_INVOKABLE function that can be called from QML to create these menus. The new class is defined thusly:

class QmlWindow : public QMainWindow
{
    Q_OBJECT
public:
    QmlWindow(QWidget *parent = NULL);
    Q_INVOKABLE QAction *addMenuItem(QString menu, QString item);
private:
    QHash<QString, QMenu *> menus;
};

The new function takes the name of a menu and the name of the menu item and returns a pointer to a newly created QAction which is triggered from the new menu item. The implementation is trivial:

QAction *QmlWindow::addMenuItem(QString menu, QString item)
{
    QMenu *theMenu;
    if(menus.contains(menu))
    {
        theMenu = menus.value(menu);
    }
    else
    {
        theMenu = menuBar()->addMenu(menu);
        menus.insert(menu, theMenu);
    }
    return theMenu->addAction(item);
}

Before we can call this function from QML, we must inject the window object into the context the QML is executed in. It is important to do this before loading the QML document.

qmlRegisterType("CustomComponents", 1, 0, "Window");
QmlWindow *window = new QmlWindow;
QmlApplicationViewer *viewer = new QmlApplicationViewer;
viewer->rootContext()->setContextProperty("window", window);
viewer->setSource(QUrl("qrc:/qml/qml/BrewPlot/main.qml"));

With this, we can now define our menu items in a QML file and define the functionality of these items. This is how I defined the three menu items in BrewPlot.

Component.onCompleted: {
    var quitItem = window.addMenuItem("File", "Quit");
    quitItem.shortcut = "Ctrl+Q";
    quitItem.triggered.connect(function() {
                                   Qt.quit();
                               });
    var clearItem = window.addMenuItem("Plotting", "Clear Data");
    clearItem.triggered.connect(function() {
                                    graph.clear();
                                    dataViewModel.clear();
                                    root.sumy = 0;
                                    root.sumxsq = 0;
                                    root.sumx = 0;
                                    root.sumxy = 0;
                                    root.n = 0;
                                    graph.setFit(0, 0, 0, 0);
                                });
    var showLeastSquares = window.addMenuItem("Plotting", "Least Squares Fit");
    showLeastSquares.checkable = true;
    showLeastSquares.triggered.connect(function() {
                                           graph.setFitVisible(showLeastSquares.checked);
                                           lsfrow.visible = showLeastSquares.checked;
                                       });
}

Those semicolons aren’t needed, but I like to have them anyway. As you can see, we have access to QAction properties such as checked and checkable, can set keyboard shortcuts, and can connect JavaScript functions to the triggered signal.

Least Squares Fit

One of the use cases I had when writing this was having the ability to measure several brews with a single brewing parameter changing and snap a trend line over the data. One of the menu items defined above toggles the visibility of a least squares fit line and a control for setting the color of the line (the line can be drawn in red, orange, yellow, green, blue, indigo, or violet). To draw the line, we calculate the required a and b values in y=ax+b which should look familiar as the formula for a line on a plane. For this, I keep track of five values as new points are added to the plot:

property real sumy : 0
property real sumxsq : 0
property real sumx : 0
property real sumxy : 0
property int n : 0

When n>1, these are used to calculate the a and b components according to:

In code, that looks like:

var a = ((root.sumy * root.sumxsq) - (root.sumx * root.sumxy)) /
        ((root.n * root.sumxsq) - Math.pow(root.sumx, 2))
var b = ((root.n * root.sumxy) - (root.sumx * root.sumy)) /
        ((root.n * root.sumxsq) - Math.pow(root.sumx, 2))

From this it is easy to determine the end points of our line.

For more details, you can find complete source code for BrewPlot on the project web page. Note that this is the first program I’ve written using QML. The program works, but there are many areas that can be improved. Don’t look to this as an example of best practices, but at the same time I’d rather not run into it on The Daily WTF.

Today I released version 1.0 of BrewPlot, an electronic coffee brewing control chart. This shouldn’t be mistaken as a serious program. It was done as a learning exercise and I’m not sufficiently interested in it that I’m going to spend a lot of time adding features. Both a Windows binary and source code have been released. I’ll probably get around to releasing a Mac build eventually. It shouldn’t be difficult to compile it on other platforms as well, but that is left as an exercise for the person who wants it on other platforms.

The functionality of this software is covered on the project site. Download links are at the top of the page.

A Brewing Control Chart in QML


A while back I decided that it would be a good idea for me to learn QML in anticipation of an upcoming project. One of the best ways to learn a new programming language is to write a simple program with it. I decided to try a program that would allow me to measure the mass of ground coffee, the mass of brewed coffee, and the %TDS and have the result plotted on a brewing control chart. It’s not a fancy demo, there aren’t any animations, and there’s certainly room for improvement both in terms of features and in how I chose to implement certain things, but while figuring things out I got a good grasp of the basics of the language and how to work with it. Full sized screenshot.