Open App
DIY Hardware Guide

Build your own Roastmark temperature probe

An ESP32-C6 with two K-type thermocouples that wirelessly streams bean and environment temperature to the Roastmark app via Bluetooth.

1. Parts list

Everything can be sourced from AliExpress, Amazon, or similar electronics suppliers. No soldering is required if you use the breakout board modules — just jumper wires.

Part Qty Notes
ESP32-C6 development board Seeed Studio XIAO ESP32C6 or similar 1 Must be C6 (not C3) for BLE 5.0 + good range. The XIAO is tiny and cheap.
MAX6675 breakout module Includes header pins 2 One for bean temp, one for environment temp. Works with 1 if you only need BT.
K-type thermocouple Usually bundled with MAX6675 modules 2 Standard probes work for ambient roasting temps up to ~400 °C
Jumper wires (female-to-female) 7 For connecting MAX6675 modules to ESP32 pins
USB-C cable 1 For flashing firmware and powering the board
Breadboard (optional) 1 Makes wiring cleaner. Not required if wiring directly.
Estimated cost: ~$25–35 CAD from AliExpress, or ~$45–60 CAD from Amazon with faster shipping. The ESP32-C6 is the most expensive part (~$8–15).

2. Wiring

Both MAX6675 modules share the same clock (SCK) and data (SO) lines. Only the chip select (CS) pin is different — this tells the ESP32 which probe to read.

MAX6675 pin Bean probe (BT) Env probe (ET)
VCC 3.3V 3.3V
GND GND GND
SCK D8 (shared) D8 (shared)
SO D9 (shared) D9 (shared)
CS D3 D4
ESP32-C6 ┌─────────────┐ │ 3.3V ├──────┬──────── MAX6675 #1 VCC (bean) │ │ └──────── MAX6675 #2 VCC (env) │ │ │ GND ├──────┬──────── MAX6675 #1 GND │ │ └──────── MAX6675 #2 GND │ │ │ D8 SCK ├──────┬──────── MAX6675 #1 SCK │ │ └──────── MAX6675 #2 SCK │ │ │ D9 SO ├──────┬──────── MAX6675 #1 SO │ │ └──────── MAX6675 #2 SO │ │ │ D3 CS1 ├────────────── MAX6675 #1 CS (bean) │ │ │ D4 CS2 ├────────────── MAX6675 #2 CS (env) └─────────────┘
One probe is fine too. If you only have one MAX6675, wire it to D3 (bean temp). The firmware handles a missing second probe gracefully — it will just report ET as "missing".

3. Reference photos

Here's what the actual build looks like using a Seeed Studio XIAO ESP32C6 and two MAX6675 modules (excuse the soldering — it's not pretty, but it works). For reference, the wire colours I used: black for GND, red for 3.3V/VCC, blue for D9/SO, yellow for D8/SCK, and orange for D3/CS1 and D4/CS2.

The board

...
XIAO ESP32C6 — front
...
Wiring — back view
...
Wiring — alternate angle

The thermocouple modules

...
Two MAX6675 modules with K-type thermocouples
...
MAX6675 breakout — close-up

A note on shared connections

Four of the wires are shared between both MAX6675 modules (VCC, GND, SCK, SO), which means you need to split one wire into two at those pins. There are a few ways to handle this:

Splicing is the cheapest option. You can strip and twist the female dupont connectors together for the shared connections. Alternatively, you can buy pre-made 1-female-to-2-female dupont splitter cables, but they tend to be surprisingly expensive for what they are. For roughly the same price as a single splitter, you can get a mixed bag of 120 dupont connectors on AliExpress — well worth it if you plan to tinker with more projects. If you have a dupont crimping tool, you can also crimp your own custom-length cables.

What it costs

AliExpress order showing parts cost
Actual AliExpress order — ESP32-C6 boards, MAX6675 modules, and thermocouples. Total came to about $67 CAD, but this includes extras (multiple boards and modules) for future builds.

4. Flash the firmware

The firmware is a standard Arduino sketch. You'll need the Arduino IDE (or PlatformIO) with ESP32 board support.

Setup Arduino IDE

  1. Install Arduino IDE 2.x Download from arduino.cc if you haven't already.
  2. Add ESP32 board support Go to File → Preferences, and add this URL to "Additional Board Manager URLs":
    https://espressif.github.io/arduino-esp32/package_esp32_index.json
    Then open Tools → Board → Board Manager, search for esp32 by Espressif, and install it.
  3. Install the MAX6675 library Go to Sketch → Include Library → Manage Libraries, search for MAX6675 (by Adafruit), and install it.
  4. Select your board Go to Tools → Board and select ESP32C6 Dev Module. Set USB CDC On Boot to Enabled (so Serial Monitor works over USB).
  5. Create a new sketch and paste the code In Arduino IDE, go to File → New Sketch. Delete the default contents, then copy and paste the entire sketch below. Save it, connect your ESP32-C6 via USB-C, select the correct port under Tools → Port, and click Upload.

The sketch

#include <max6675.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// =========================
// MAX6675 pin setup
// =========================

// Shared SPI-style lines
const int thermoSO   = D9;  // SO / MISO shared
const int thermoSCK  = D8;  // SCK shared

// Separate chip select pins
const int beanCS     = D3;  // Probe 1 = Bean Temp
const int envCS      = D4;  // Probe 2 = Env Temp

MAX6675 beanProbe(thermoSCK, beanCS, thermoSO);
MAX6675 envProbe(thermoSCK, envCS, thermoSO);

// =========================
// BLE UUIDs
// =========================
#define SERVICE_UUID       "7E100001-3D7A-4B6F-9E1C-6B1A5A100001"
#define BT_CHAR_UUID       "7E100002-3D7A-4B6F-9E1C-6B1A5A100002"
#define ET_CHAR_UUID       "7E100003-3D7A-4B6F-9E1C-6B1A5A100003"
#define STATUS_CHAR_UUID   "7E100004-3D7A-4B6F-9E1C-6B1A5A100004"

BLECharacteristic* btCharacteristic;
BLECharacteristic* etCharacteristic;
BLECharacteristic* statusCharacteristic;

bool deviceConnected = false;

// =========================
// BLE callbacks
// =========================
class ServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) override {
    deviceConnected = true;
    Serial.println("BLE client connected");
  }

  void onDisconnect(BLEServer* pServer) override {
    deviceConnected = false;
    Serial.println("BLE client disconnected");
    BLEDevice::startAdvertising();
    Serial.println("Advertising restarted");
  }
};

// =========================
// Helper functions
// =========================
bool isValidTemp(float t) {
  return !isnan(t) && t > -20.0 && t < 1200.0;
}

void setCharacteristicFloat(BLECharacteristic* characteristic, float value) {
  char buffer[16];
  dtostrf(value, 0, 2, buffer);
  characteristic->setValue(buffer);
}

void setupBLE() {
  BLEDevice::init("Roastmark Probe");

  BLEServer* server = BLEDevice::createServer();
  server->setCallbacks(new ServerCallbacks());

  BLEService* service = server->createService(SERVICE_UUID);

  btCharacteristic = service->createCharacteristic(
    BT_CHAR_UUID,
    BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_NOTIFY
  );
  btCharacteristic->addDescriptor(new BLE2902());
  btCharacteristic->setValue("0.00");

  etCharacteristic = service->createCharacteristic(
    ET_CHAR_UUID,
    BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_NOTIFY
  );
  etCharacteristic->addDescriptor(new BLE2902());
  etCharacteristic->setValue("NaN");

  statusCharacteristic = service->createCharacteristic(
    STATUS_CHAR_UUID,
    BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_NOTIFY
  );
  statusCharacteristic->addDescriptor(new BLE2902());
  statusCharacteristic->setValue("BT:0 ET:missing");

  service->start();

  BLEAdvertising* advertising = BLEDevice::getAdvertising();
  advertising->addServiceUUID(SERVICE_UUID);
  advertising->setScanResponse(true);
  BLEDevice::startAdvertising();

  Serial.println("BLE advertising as: Roastmark Probe");
}

// =========================
// Setup
// =========================
void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("Starting Roastmark 2-probe BLE firmware...");
  Serial.println("Bean probe: SCK=D8 CS=D3 SO=D9");
  Serial.println("Env  probe: SCK=D8 CS=D4 SO=D9");
  Serial.println("Probe 2 can be missing for now.");

  delay(500);
  setupBLE();
}

// =========================
// Main loop
// =========================
void loop() {
  float beanTemp = beanProbe.readCelsius();
  float envTemp  = envProbe.readCelsius();

  bool beanOk = isValidTemp(beanTemp);
  bool envOk  = isValidTemp(envTemp);

  // Serial output
  Serial.print("BT: ");
  if (beanOk) {
    Serial.print(beanTemp, 2);
    Serial.print(" C");
  } else {
    Serial.print("invalid");
  }

  Serial.print(" | ET: ");
  if (envOk) {
    Serial.print(envTemp, 2);
    Serial.println(" C");
  } else {
    Serial.println("missing/invalid");
  }

  // Update BT characteristic
  if (beanOk) {
    setCharacteristicFloat(btCharacteristic, beanTemp);
  } else {
    btCharacteristic->setValue("NaN");
  }

  // Update ET characteristic
  if (envOk) {
    setCharacteristicFloat(etCharacteristic, envTemp);
  } else {
    etCharacteristic->setValue("NaN");
  }

  // Update status characteristic
  String status = "BT:";
  status += beanOk ? "ok" : "bad";
  status += " ET:";
  status += envOk ? "ok" : "missing";
  statusCharacteristic->setValue(status.c_str());

  // Notify connected client
  if (deviceConnected) {
    btCharacteristic->notify();
    etCharacteristic->notify();
    statusCharacteristic->notify();
  }

  delay(1000);  // update once per second
}

Verify it works

Open Tools → Serial Monitor at 115200 baud. You should see output like:

Starting Roastmark 2-probe BLE firmware...
Bean probe: SCK=D8 CS=D3 SO=D9
Env  probe: SCK=D8 CS=D4 SO=D9
BLE advertising as: Roastmark Probe
BT: 24.50 C | ET: 25.10 C
BT: 24.75 C | ET: 25.00 C
Board not detected? Some ESP32-C6 boards need you to hold the BOOT button while plugging in USB to enter flash mode. Release it after upload starts.

5. Connect to the app

Once the firmware is running and you see temperature readings in Serial Monitor, the probe is ready to pair.

  1. Open the Roastmark app On your Android device, open the app and start a new roast.
  2. Tap "Show" on the probe panel At the top of the roasting screen, you'll see the Bluetooth probe status bar. Tap Show to expand it.
  3. Tap "Scan & Connect" The app will scan for nearby BLE devices. It looks specifically for a device named "Roastmark Probe" — your ESP32.
  4. Auto-logging starts Once connected, the probe panel minimizes and shows live BT/ET readings. Temperature is automatically logged every second while the roast timer is running.
Both temps on the graph. Bean temperature appears as the main line on your roast graph. Environment temperature is stored with each data point and shown in the data point detail view.

6. Troubleshooting

Serial Monitor shows "BT: invalid"
Check the bean probe wiring. Make sure CS is on D3, SCK on D8, SO on D9, and the thermocouple is plugged firmly into the MAX6675 module. Also confirm you're using 3.3V (not 5V).
Serial Monitor shows "ET: missing/invalid"
This is normal if you only have one probe connected. If you do have a second probe, check that its CS wire goes to D4 (not D3).
App says "Could not find Roastmark Probe"
Make sure the ESP32 is powered on and Serial Monitor shows "BLE advertising as: Roastmark Probe". Try moving closer — BLE range is roughly 10 metres. Also ensure Bluetooth and Location are both enabled on your phone (Android requires Location for BLE scanning).
Connects for a split second then disconnects
This was a known app bug (fixed in v1.1+). Update to the latest version of the Roastmark app. If still happening, try power-cycling the ESP32.
Temperature readings seem stuck or delayed
The MAX6675 updates roughly once per second. If readings are completely frozen, try unplugging and re-plugging the thermocouple from the module — a loose connection can cause stale reads.
Upload fails in Arduino IDE
Hold the BOOT button on the ESP32-C6 while clicking Upload, then release after "Connecting..." appears. Also double-check that "ESP32C6 Dev Module" is selected, not a different ESP32 variant.