Roasting coffee is an art that depends on precision—knowing exactly when and how the temperature changes throughout the roast makes all the difference in flavor. In this project, we build a DIY temperature-monitoring dashboard using a Flipper device along with a MAX31855 thermocouple amplifier and a K-type thermocouple sensor. The project not only measures and logs the temperature each second but also displays a live timeline graph of the temperature readings to help you visualize the roast progression.

Project Overview

This project integrates the following elements:

  • Flipper Device: A versatile handheld tool that can run custom firmware. Here, it serves as the central unit that reads temperature data, stores it in an internal timeline, and displays the readings along with a live graph.
  • K-Type Thermocouple & MAX31855: While the provided code example originally used a DS18B20 sensor, the updated design leverages a K-type thermocouple. K-type sensors are industrial-grade devices widely used in high-temperature applications. The MAX31855 chip is essential because it amplifies the weak thermocouple signal, performs cold-junction compensation, and outputs a digital temperature reading that the microcontroller can use.
  • Interactive GUI: The firmware includes a graphical user interface (GUI) that displays a running timer, current temperature, and a simple line graph showing the recent history of temperature measurements.
  • Roasting in a Popcorn Machine: Although unconventional, roasting coffee in a popcorn machine can work if the heat distribution is modified properly. By instrumenting the device with your thermometer setup, you can monitor and adjust the roast curve in real time.

The Code Explained

The following code is a complete firmware application written in C. It continuously measures temperature every second, stores each sample, and displays a timeline along with a live graph on the device’s screen. You can modify it to support your K-type thermocouple by interfacing the MAX31855, which converts the thermocouple’s analog output into a digital temperature reading.

Key Features of the Code

  • Temperature Sampling:
    The code uses a dedicated reader thread to trigger new temperature measurements every second. Each sample is stored in an array, and if the array becomes full, older values are shifted out to make space for the latest readings.

  • Graphical Display:
    The GUI callback function draws a title, elapsed time, current temperature, and a dynamic line graph that plots the latest temperature measurements. The graph scales according to the minimum and maximum temperatures in the data, ensuring that fluctuations are clearly visible.

  • User Interaction:
    The firmware listens for button events; for example, pressing the “Back” key exits the application gracefully by stopping the reader thread.

  • Adaptability for K-Type Thermocouple:
    While the example code is based on a DS18B20 sensor, switching to a MAX31855 K-type configuration primarily requires updating the sensor initialization and data reading functions. The MAX31855 communicates over SPI, so you’d replace the one-wire functions with SPI read/write commands appropriate to your chosen microcontroller and MAX31855 breakout board.

Below is the code that ties these elements together:


/**
 * @file example_thermo_timeline_graph.c
 * @brief Updated 1-Wire thermometer example with timeline, timer, and graph display.
 *
 * This updated example reads the temperature from a DS18B20 1-wire thermometer every second,
 * stores each sample, and displays:
 *   - A running timer
 *   - The current temperature reading
 *   - A timeline history
 *   - A simple line graph of recent temperature readings
 *
 * Note: For a K-type thermocouple with a MAX31855, replace the one-wire communication functions
 *       with SPI-based communication that reads data from the MAX31855.
 *
 * References:
 * [1] DS18B20 Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/DS18B20.pdf
 * [2] MAX31855 Datasheet: https://www.maximintegrated.com/en/products/analog/data-converters/thermocouple-amplifiers/MAX31855.html
 */
 
#include <gui/gui.h>
#include <gui/view_port.h>
#include <core/thread.h>
#include <core/kernel.h>
#include <locale/locale.h>
#include <one_wire/maxim_crc.h>
#include <one_wire/one_wire_host.h>
#include <power/power_service/power.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
 
#define UPDATE_PERIOD_MS 1000UL
#define TEXT_STORE_SIZE  64U
 
#define DS18B20_CMD_SKIP_ROM        0xccU
#define DS18B20_CMD_CONVERT         0x44U
#define DS18B20_CMD_READ_SCRATCHPAD 0xbeU
 
#define DS18B20_CFG_RESOLUTION_POS  5U
#define DS18B20_CFG_RESOLUTION_MASK 0x03U
#define DS18B20_DECIMAL_PART_MASK   0x0fU
 
#define DS18B20_SIGN_MASK 0xf0U
 
/* Possible GPIO pin choices:
 - gpio_ext_pc0, gpio_ext_pc1, gpio_ext_pc3, gpio_ext_pb2, gpio_ext_pb3,
   gpio_ext_pa4, gpio_ext_pa6, gpio_ext_pa7, gpio_ibutton
*/
#define THERMO_GPIO_PIN (gpio_ibutton)
 
/* Maximum number of records to store */
#define MAX_RECORDS 120U
 
/* Flags which the reader thread responds to */
typedef enum {
    ReaderThreadFlagExit = 1,
} ReaderThreadFlag;
 
typedef union {
    struct {
        uint8_t temp_lsb;         /* Least significant byte of the temperature */
        uint8_t temp_msb;         /* Most significant byte of the temperature */
        uint8_t user_alarm_high;  /* User register 1 (Temp high alarm) */
        uint8_t user_alarm_low;   /* User register 2 (Temp low alarm) */
        uint8_t config;           /* Configuration register */
        uint8_t reserved[3];      /* Not used */
        uint8_t crc;              /* CRC checksum for error detection */
    } fields;
    uint8_t bytes[9];
} DS18B20Scratchpad;
 
/* Application context structure */
typedef struct {
    Gui* gui;
    ViewPort* view_port;
    FuriThread* reader_thread;
    FuriMessageQueue* event_queue;
    OneWireHost* onewire;
    Power* power;
    float temp_celsius;
    bool has_device;
    /* Fields to store timeline and timer */
    uint32_t elapsed_time_seconds;
    float temperature_records[MAX_RECORDS];
    size_t record_count;
} ExampleThermoContext;
 
/*************** 1-Wire Communication and Processing *****************/
 
static void example_thermo_request_temperature(ExampleThermoContext* context) {
    OneWireHost* onewire = context->onewire;
    FURI_CRITICAL_ENTER();
    bool success = false;
    do {
        if(!onewire_host_reset(onewire)) break;
        onewire_host_write(onewire, DS18B20_CMD_SKIP_ROM);
        onewire_host_write(onewire, DS18B20_CMD_CONVERT);
        success = true;
    } while(false);
    context->has_device = success;
    FURI_CRITICAL_EXIT();
}
 
static void example_thermo_read_temperature(ExampleThermoContext* context) {
    if(!context->has_device) {
        return;
    }
    OneWireHost* onewire = context->onewire;
    FURI_CRITICAL_ENTER();
    bool success = false;
    do {
        DS18B20Scratchpad buf;
        size_t attempts_left = 10;
        do {
            if(!onewire_host_reset(onewire)) continue;
            onewire_host_write(onewire, DS18B20_CMD_SKIP_ROM);
            onewire_host_write(onewire, DS18B20_CMD_READ_SCRATCHPAD);
            onewire_host_read_bytes(onewire, buf.bytes, sizeof(buf.bytes));
            const uint8_t crc = maxim_crc8(buf.bytes, sizeof(buf.bytes) - 1, MAXIM_CRC8_INIT);
            if(crc == buf.fields.crc) break;
        } while(--attempts_left);
        if(attempts_left == 0) break;
        const uint8_t resolution_mode = (buf.fields.config >> DS18B20_CFG_RESOLUTION_POS) & DS18B20_CFG_RESOLUTION_MASK;
        const uint8_t decimal_mask =
            (DS18B20_DECIMAL_PART_MASK << (DS18B20_CFG_RESOLUTION_MASK - resolution_mode)) & DS18B20_DECIMAL_PART_MASK;
        const uint8_t integer_part = (buf.fields.temp_msb << 4U) | (buf.fields.temp_lsb >> 4U);
        const uint8_t decimal_part = buf.fields.temp_lsb & decimal_mask;
        const bool is_negative = (buf.fields.temp_msb & DS18B20_SIGN_MASK) != 0;
        const float temp_celsius_abs = integer_part + decimal_part / 16.f;
        context->temp_celsius = is_negative ? -temp_celsius_abs : temp_celsius_abs;
        success = true;
    } while(false);
    context->has_device = success;
    FURI_CRITICAL_EXIT();
}
 
/* Reader thread: requests measurements, reads temperature, updates timeline and timer */
static int32_t example_thermo_reader_thread_callback(void* ctx) {
    ExampleThermoContext* context = ctx;
    for(;;) {
        example_thermo_request_temperature(context);
        const uint32_t flags =
            furi_thread_flags_wait(ReaderThreadFlagExit, FuriFlagWaitAny, UPDATE_PERIOD_MS);
        if(flags != (unsigned)FuriFlagErrorTimeout) break;
        example_thermo_read_temperature(context);
        /* Store temperature in timeline */
        if(context->record_count < MAX_RECORDS) {
            context->temperature_records[context->record_count++] = context->temp_celsius;
        } else {
            memmove(context->temperature_records,
                    context->temperature_records + 1,
                    (MAX_RECORDS - 1) * sizeof(float));
            context->temperature_records[MAX_RECORDS - 1] = context->temp_celsius;
        }
        /* Increase elapsed time */
        context->elapsed_time_seconds++;
    }
    return 0;
}
 
/*************** GUI, Input and Main Loop *****************/
 
/* Drawing callback: displays title, timer, current temp, and a graph of recent readings */
static void example_thermo_draw_callback(Canvas* canvas, void* ctx) {
    ExampleThermoContext* context = ctx;
    char text_store[TEXT_STORE_SIZE];
    const size_t canvas_w = canvas_width(canvas);
    const size_t canvas_h = canvas_height(canvas);
    const size_t middle_x = canvas_w / 2U;
 
    // Draw title, timer, and current temperature.
    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str_aligned(canvas, middle_x, 10, AlignCenter, AlignBottom, "Coffee Roasting");
 
    canvas_set_font(canvas, FontSecondary);
    snprintf(text_store, TEXT_STORE_SIZE, "Time: %lus", context->elapsed_time_seconds);
    canvas_draw_str_aligned(canvas, middle_x, 22, AlignCenter, AlignBottom, text_store);
 
    {
        float temp;
        char temp_units;
        switch(locale_get_measurement_unit()) {
            case LocaleMeasurementUnitsMetric:
                temp = context->temp_celsius;
                temp_units = 'C';
                break;
            case LocaleMeasurementUnitsImperial:
                temp = locale_celsius_to_fahrenheit(context->temp_celsius);
                temp_units = 'F';
                break;
            default:
                furi_crash("Illegal measurement units");
        }
        canvas_set_font(canvas, FontSecondary);
        snprintf(text_store, TEXT_STORE_SIZE, "Temp: %+.1f%c", temp, temp_units);
        canvas_draw_str_aligned(canvas, middle_x, 34, AlignCenter, AlignBottom, text_store);
    }
 
    /* ---- Graph drawing ---- */
    // Define margins and graph area.
    const uint8_t margin_left = 5;
    const uint8_t margin_right = 5;
    const uint8_t margin_top = 38;   // below text
    const uint8_t margin_bottom = 5;
    const uint8_t graph_x = margin_left;
    const uint8_t graph_y = margin_top;
    const uint8_t graph_w = canvas_w - margin_left - margin_right;
    const uint8_t graph_h = canvas_h - margin_top - margin_bottom;
 
    // Draw a border around the graph area
    canvas_draw_rect(canvas, graph_x, graph_y, graph_w, graph_h);
 
    // Determine the number of data points to plot.
    size_t total_points = context->record_count;
    size_t num_points = (total_points < graph_w) ? total_points : graph_w;
    if(num_points < 2) {
        return; // Not enough data to plot.
    }
    size_t start_index = total_points > num_points ? total_points - num_points : 0;
    
    // Compute the minimum and maximum values for scaling.
    float min_val = context->temperature_records[start_index];
    float max_val = context->temperature_records[start_index];
    for(size_t i = start_index; i < total_points; i++) {
        float val = context->temperature_records[i];
        if(val < min_val) { min_val = val; }
        if(val > max_val) { max_val = val; }
    }
    // Prevent division by zero.
    if(min_val == max_val) {
        min_val -= 0.5f;
        max_val += 0.5f;
    }
 
    // Plot the graph by drawing lines between consecutive data points.
    int prev_x = graph_x;
    int prev_y = graph_y + graph_h - (int)(((context->temperature_records[start_index] - min_val) / (max_val - min_val)) * graph_h);
    for(size_t i = start_index + 1; i < total_points; i++) {
        uint8_t index = (uint8_t)(i - start_index);
        int curr_x = graph_x + ((index * graph_w) / (num_points - 1));
        int curr_y = graph_y + graph_h - (int)(((context->temperature_records[i] - min_val) / (max_val - min_val)) * graph_h);
        canvas_draw_line(canvas, prev_x, prev_y, curr_x, curr_y);
        prev_x = curr_x;
        prev_y = curr_y;
    }
}
 
/* Input callback: passes button press events into the event queue */
static void example_thermo_input_callback(InputEvent* event, void* ctx) {
    ExampleThermoContext* context = ctx;
    furi_message_queue_put(context->event_queue, event, FuriWaitForever);
}
 
/* Main loop: starts the reader thread and handles user input */
static void example_thermo_run(ExampleThermoContext* context) {
    power_enable_otg(context->power, true);
    onewire_host_start(context->onewire);
    furi_thread_start(context->reader_thread);
    for(bool is_running = true; is_running;) {
        InputEvent event;
        const FuriStatus status =
            furi_message_queue_get(context->event_queue, &event, FuriWaitForever);
        if((status != FuriStatusOk) || (event.type != InputTypeShort)) {
            continue;
        }
        if(event.key == InputKeyBack) {
            is_running = false;
        }
    }
    furi_thread_flags_set(furi_thread_get_id(context->reader_thread), ReaderThreadFlagExit);
    furi_thread_join(context->reader_thread);
    onewire_host_stop(context->onewire);
    power_enable_otg(context->power, false);
}
 
/* Allocate and initialize the application context */
static ExampleThermoContext* example_thermo_context_alloc(void) {
    ExampleThermoContext* context = malloc(sizeof(ExampleThermoContext));
    context->view_port = view_port_alloc();
    view_port_draw_callback_set(context->view_port, example_thermo_draw_callback, context);
    view_port_input_callback_set(context->view_port, example_thermo_input_callback, context);
    context->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
    context->reader_thread = furi_thread_alloc();
    furi_thread_set_stack_size(context->reader_thread, 1024U);
    furi_thread_set_context(context->reader_thread, context);
    furi_thread_set_callback(context->reader_thread, example_thermo_reader_thread_callback);
    context->gui = furi_record_open(RECORD_GUI);
    gui_add_view_port(context->gui, context->view_port, GuiLayerFullscreen);
    context->onewire = onewire_host_alloc(&THERMO_GPIO_PIN);
    context->power = furi_record_open(RECORD_POWER);
    context->elapsed_time_seconds = 0;
    context->record_count = 0;
    memset(context->temperature_records, 0, sizeof(context->temperature_records));
    return context;
}
 
/* Free allocated resources */
static void example_thermo_context_free(ExampleThermoContext* context) {
    view_port_enabled_set(context->view_port, false);
    gui_remove_view_port(context->gui, context->view_port);
    onewire_host_free(context->onewire);
    furi_thread_free(context->reader_thread);
    furi_message_queue_free(context->event_queue);
    view_port_free(context->view_port);
    furi_record_close(RECORD_GUI);
    furi_record_close(RECORD_POWER);
}
 
/* Application entry point */
int32_t example_thermo_main(void* p) {
    UNUSED(p);
    ExampleThermoContext* context = example_thermo_context_alloc();
    example_thermo_run(context);
    example_thermo_context_free(context);
    return 0;
}

Choosing the Right Thermocouple and MAX31855 Integration

For high-temperature measurements like those in coffee roasting, a K-type thermocouple is an excellent choice. Here are some recommendations and tips:

  • Sensor Recommendation:
    Consider a rugged K-type thermocouple such as the ones offered by Omega or other reliable sensor manufacturers. These sensors are designed for high temperatures (up to approximately 1,260°C or 2,300°F), which more than covers coffee roasting needs.

  • MAX31855 Thermocouple Amplifier:
    The MAX31855 is specifically designed to work with thermocouples. It digitizes the tiny voltage output of the K-type thermocouple, performs cold-junction compensation, and communicates via SPI. There are readily available breakout boards that simplify interfacing with microcontrollers and devices like the Flipper.

    • When integrating with the Flipper device, replace the one-wire communication routines in the example code with SPI routines to fetch data from the MAX31855.
    • Many open-source libraries for MAX31855 are available; these can be adapted to ensure accurate readings.
  • Wiring and Installation:
    Make sure the thermocouple’s wiring is properly shielded and that the connector is secure. In a high-temperature environment, such as inside a modified popcorn machine, proper insulation is important. Use appropriate high-temperature wires and connectors as recommended by your sensor’s manufacturer.

Conclusion

This project provides an excellent starting point for creating a temperature-monitoring dashboard for coffee roasting with a Flipper device. The code example demonstrates how to collect temperature data periodically, display a live timeline with a graph, and manage user inputs. With minor modifications to use SPI communication for the MAX31855, you can integrate a robust K-type thermocouple sensor into your setup.

Whether you’re a coffee connoisseur fine-tuning your roast or a hardware enthusiast interested in data visualization on a portable device, this project offers both a practical application and a fun way to experiment with embedded systems, sensor interfacing, and graphical display programming.

Happy roasting, and enjoy the blend of technology and coffee artistry!


This article covers the project’s goals, detailed code with a graph display, and guidance on selecting the right thermocouple hardware for your MAX31855-based temperature monitor.