Skip to content

FROTHED

Brandon King| Feb, 2025 - Current

Thesis

The matcha market is set to double to 9 Billion by 2030, with an estimated 20 million people drinking it daily. The Problem? There is no keurig equivalent, no machine which provides matcha at the touch of a button


I took this idea for an all in one machine from napkin sketch to prototype in just 3 months. I wanted to quickly get to an MVP which provided the same consistency and foam as a normal matcha shot.

Design

I started the napkin sketch phase as I always do:

  1. Patent Research
  2. Open source docs
  3. Technical Papers


From there, I had a few problems to deal with. First, most consumer hardware is injection molded. This is mainly for cost savings, but also the cheapest way to make complex food safe parts. Since low volume injection molding is still very cost prohibitive, I designed this prototype to use CNC for all food safe parts. Additionally you’ll use aluminum extrusions used for a box shaped base. This was done for speed of design and assembly.


The main IP of this machine lies in the mixing chamber geoemtry, inlet and outlets, and heating. I designed, simulated, and built 6 potential mixing chamber designs, along with 5 different mixing head geometries. Since this is an on going project for me with real market potential, I will not dive deep into the weeds here. I have attached a photo with some geometry omitted, so you can get a sense of the basic design.


Microbot CAD Model

Skeleton CAD


Firmware

For the electronics side of things, I went with a skeleton PCB. For the first prototype, I just wanted to prove consistency and foam levels. So it really didn’t make sense, for the sake of time, to design, test, and prototype a custom pcb board.


For the brains I choose the ESP32 to allow for OTA updates and remote diagnostics on heating temps, flow rates, etc.


For the firmware I choose ESPAsyncWebServer, an open source wifi-enabled host. The web server sets up mutliple HTTP endpoints that map to specific functions on the machine, so I can control and test from across the room. There are also manual controls enabled, which allow for one shot (short press), or an automated cleaning flush (long press). Most of the code is pretty basic compared to stuff I’ve written (neural networks, MPC, etc).

Code Snippet
void loop(void) {
// Read button state (LOW when pressed due to pull-up)
bool buttonPressed = (digitalRead(BUTTON_PIN) == LOW);

// Button press detection
if (buttonPressed && !buttonWasPressed) {
  // Button was just pressed
  buttonPressStartTime = millis();
  buttonWasPressed = true;
} else if (!buttonPressed && buttonWasPressed) {
  // Button was just released
  unsigned long pressDuration = millis() - buttonPressStartTime;

  if (pressDuration >= LONG_PRESS_DURATION) {
    // Long press detected - start flush sequence
    if (currentFlushState == FLUSH_IDLE) {
      currentFlushState = FLUSH_PUMP_IN;
      flowSensor.resetTotalVolume();  // Reset volume counter
      Serial.println("Flush sequence started via long press");
    }
  } else {
    // Short press detected - start relay sequence first
    Serial.println("Button pressed! Starting relay sequence");
    brewButtonRelayTimer = millis();
    brewButtonRelayActive = true;

    // Start brewing process after relay sequence
    temperatureControlEnabled = true;
    manualPumpControl = false;  // Reset manual control when starting brewing process
    currentBrewingState = IDLE;  // Reset brewing state to start from beginning
  }
  buttonWasPressed = false;
}

blinkLED(LED_PIN, 500);  // Blink LED every 500ms

// Handle flush sequence state machine
switch (currentFlushState) {
  case FLUSH_IDLE:
    // Do nothing, waiting for sequence to start
    break;

  case FLUSH_PUMP_IN:
    // Run pump in until target volume is reached
    relayControl.setRelayState(25, HIGH);  // Pump IN ON

    if (flowSensor.getTotalVolume() >= TARGET_VOLUME) {
      relayControl.setRelayState(25, LOW);  // Pump IN OFF
      currentFlushState = FLUSH_STIR;
      flushTimer = millis();
      relayControl.setRelayState(27, HIGH);  // Turn on Stir Motor
    }
    break;

  case FLUSH_STIR:
    // Run stir motor for 5 seconds
    if (millis() - flushTimer >= FLUSH_STIR_TIME) {
      relayControl.setRelayState(27, LOW);  // Stir Motor OFF
      currentFlushState = FLUSH_PUMP_OUT;
      flushTimer = millis();
      relayControl.setRelayState(26, HIGH);  // Turn on Pump OUT
    }
    break;

  case FLUSH_PUMP_OUT:
    // Run pump out for 8 seconds
    if (millis() - flushTimer >= FLUSH_PUMP_OUT_TIME) {
      relayControl.setRelayState(26, LOW);  // Pump OUT OFF
      currentFlushState = FLUSH_IDLE;
      flowSensor.resetTotalVolume();  // Reset volume counter
    }
    break;
}

// Handle brewing process state machine
if (temperatureControlEnabled && !manualPumpControl) {  // Only run if not in manual control
  float currentTemp = readThermistor();

  switch (currentBrewingState) {
    case IDLE:
      // Start the brewing process
      currentBrewingState = HEATING;
      ssrControl.setTemperatureThreshold(TARGET_TEMP);
      break;

    case HEATING:
      // Use SSRControl to maintain temperature
      ssrControl.update(currentTemp);

      if (currentTemp >= TARGET_TEMP) {
        currentBrewingState = PUMPING;
        pumpPulseTimer = millis();  // Initialize pump pulse timer
      }
      break;

    case PUMPING:
      // Continue maintaining temperature while pumping
      ssrControl.update(currentTemp);

      // Handle pump pulsing
      unsigned long currentPumpTime = millis();
      unsigned long pumpElapsedTime = currentPumpTime - pumpPulseTimer;
      unsigned long pumpCycleTime = pumpElapsedTime % (PUMP_ON_TIME + PUMP_OFF_TIME);

      if (pumpCycleTime < PUMP_ON_TIME) {
          relayControl.setRelayState(25, HIGH);  // Pump ON
      } else {
          relayControl.setRelayState(25, LOW);   // Pump OFF
      }

      if (flowSensor.getTotalVolume() >= TARGET_VOLUME) {
        relayControl.setRelayState(25, LOW);  // Ensure pump is off
        relayControl.setRelayState(33, HIGH); // Turn on Auger
        augerActive = true;
        augerTimer = millis();
        currentBrewingState = IDLE;
        temperatureControlEnabled = false;  // End the brewing process
        flowSensor.resetTotalVolume();  // Reset the total volume counter
      }
      break;
  }
} else {
  // Only turn off SSR when control is disabled
  digitalWrite(12, LOW);  // Pin 12 is the default SSR pin
  currentBrewingState = IDLE;
}

// Check if auger timer has expired
if (augerActive && (millis() - augerTimer >= 20000)) {
  relayControl.setRelayState(33, LOW);  // Turn off Auger after 2 seconds
  augerActive = false;
  // Start stir motor sequence
  relayControl.setRelayState(27, HIGH); // Turn on Stir Motor
  stirMotorActive = true;
  stirMotorTimer = millis();
  stirMotorPulseCount = 0;
}

// Handle stir motor pulsing
if (stirMotorActive) {
  unsigned long currentTime = millis();
  unsigned long elapsedTime = currentTime - stirMotorTimer;
  unsigned long cycleTime = elapsedTime % (STIR_MOTOR_ON_TIME + STIR_MOTOR_OFF_TIME);

  if (stirMotorPulseCount < STIR_MOTOR_PULSE_COUNT) {
    if (cycleTime < STIR_MOTOR_ON_TIME) {
      relayControl.setRelayState(27, HIGH); // Motor ON
      stirMotorPulseCounted = false;        // Reset count flag
    } else {
      relayControl.setRelayState(27, LOW);  // Motor OFF

      if (!stirMotorPulseCounted) {
        stirMotorPulseCount++;
        stirMotorPulseCounted = true;
        Serial.print("Stir motor pulse count: ");
        Serial.println(stirMotorPulseCount);
      }
    }
  } else {
    // We've completed all pulses
    relayControl.setRelayState(27, LOW);  // Ensure motor is off
    stirMotorActive = false;
    Serial.println("Stir motor sequence complete");

    // Start the exit valve sequence
    relayControl.setRelayState(32, HIGH);  // Turn on exit valve relay
    valveRelayActive = true;
    valveRelayTimer = millis();

    // Set H-bridge for OPEN
    digitalWrite(22, LOW);
    digitalWrite(23, HIGH);
    relayControl.setRelayState(23, LOW);
    relayControl.setRelayState(22, HIGH);
  }
}

// Handle valve and pump out sequence
if (valveRelayActive) {
  unsigned long currentTime = millis();
  unsigned long elapsedTime = currentTime - valveRelayTimer;

  // Allow pump out to happen regardless of temperature control state
  if (elapsedTime >= VALVE_OPEN_TIME && !pumpOutActive) {
      relayControl.setRelayState(26, HIGH);  // Use pin 26 for PUMP OUT
      pumpOutActive = true;
  }

  if (elapsedTime >= (VALVE_OPEN_TIME + PUMP_OUT_TIME)) {
      relayControl.setRelayState(32, LOW);  // Turn off exit valve
      if (pumpOutActive) {
          relayControl.setRelayState(26, LOW);  // Turn off PUMP OUT (pin 26)
          pumpOutActive = false;
      }
      valveRelayActive = false;
  }
}

// Update flow sensor readings every second using non-blocking timer
if (millis() - flowSensorTimer >= FLOW_SENSOR_INTERVAL) {
  currentFlowRate = flowSensor.readFlowRate();
  flowSensorTimer = millis();
}

// Handle brew button relay control
if (buttonWasPressed && !brewButtonRelayActive) {
    brewButtonRelayTimer = millis();
    brewButtonRelayActive = true;
}

if (brewButtonRelayActive) {
    unsigned long currentTime = millis();
    unsigned long elapsedTime = currentTime - brewButtonRelayTimer;

    if (elapsedTime >= BREW_BUTTON_RELAY_DELAY && elapsedTime < (BREW_BUTTON_RELAY_DELAY + BREW_BUTTON_RELAY_DURATION)) {
        relayControl.setRelayState(32, true);  // Turn on relay
    } else if (elapsedTime >= (BREW_BUTTON_RELAY_DELAY + BREW_BUTTON_RELAY_DURATION)) {
        relayControl.setRelayState(32, false);  // Turn off relay
        brewButtonRelayActive = false;
    }
}

Testing

Even though Testing and Design are two separate headers in this project, they occured simultaneously. This is a core belief of mine: that design and hardware testing must be intertwined. Often these are separated which leads to delayed feedback loops.

For initial testing I focused on shot temperature, foaming, and consistency. With some tweaking of the thermoblock (heating element) and the dosing mechanism, I was getting consistent matcha shots everytime!


Microbot CAD Model

Skeleton Assembly



Microbot CAD Model

First Shot


No Foam Test

Future Plans

I am now focused on an injection molded design, which allows for mass manufacturing. This reworked design will features multiple improvements:

  1. Integrated waste water container - built in flushes prevent dirty shots

  2. Single or double shot buttons - no power button, auto on

  3. Easy access hopper - allows for dumping an entire tin of powder

Below you can see a rough CAD of what the injection molded product will resemble. I took inspiration from the cloud plates and mugs that have gone viral recently.

Microbot CAD Model

Render

Microbot CAD Model

Inspo

Comments