Community project

Six-Product Smart Refill Vending Machine

ESP32
Photo of Six-Product Smart Refill Vending Machine
Generated with AI

dune Ai

Published October 31, 2026

This smart refill vending machine dispenses six products—water, milk, oil, detergent, dish soap, and hand sanitizer—with weight-based portion control and real-time monitoring. Built around an ESP32-S3 microcontroller, the machine features a touchscreen interface, six independent load cells for accurate dispensing, temperature monitoring for refrigerated products, leak detection, and networked inventory tracking via a backend API.

The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for building the control electronics, configuring the solenoid drivers, calibrating the load cells, and deploying the firmware. Makers will learn how to integrate safety systems including an emergency stop, refrigeration controls, and isolation relays while ensuring proper separation of electrical zones for safe operation.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

ILI9341 TFT Touchscreen

240x320 SPI TFT display using the ILI9341 LCD controller with an XPT2046 resistive touch controller sharing the SPI bus

1

Adafruit I2C to 8 Channel Solenoid Driver

I2C breakout (STEMMA QT / Qwiic) that drives up to eight solenoids via MOSFET outputs using an MCP23017 GPIO expander. Additional GPIO pins are broken out on a lower header row for general use.

1

Adafruit I2C to 8 Channel Solenoid Driver

I2C breakout (STEMMA QT / Qwiic) that drives up to eight solenoids via MOSFET outputs using an MCP23017 GPIO expander. Additional GPIO pins are broken out on a lower header row for general use.

1

HX711 Load Cell Amplifier

24-bit ADC front-end for strain-gauge load cells and weigh scales. DT/DOUT and SCK are MCU-facing pins; E+/E-/A+/A- connect to the load cell bridge.

1

HX711 Load Cell Amplifier

24-bit ADC front-end for strain-gauge load cells and weigh scales. DT/DOUT and SCK are MCU-facing pins; E+/E-/A+/A- connect to the load cell bridge.

1

HX711 Load Cell Amplifier

24-bit ADC front-end for strain-gauge load cells and weigh scales. DT/DOUT and SCK are MCU-facing pins; E+/E-/A+/A- connect to the load cell bridge.

1

HX711 Load Cell Amplifier

24-bit ADC front-end for strain-gauge load cells and weigh scales. DT/DOUT and SCK are MCU-facing pins; E+/E-/A+/A- connect to the load cell bridge.

1

HX711 Load Cell Amplifier

24-bit ADC front-end for strain-gauge load cells and weigh scales. DT/DOUT and SCK are MCU-facing pins; E+/E-/A+/A- connect to the load cell bridge.

1

HX711 Load Cell Amplifier

24-bit ADC front-end for strain-gauge load cells and weigh scales. DT/DOUT and SCK are MCU-facing pins; E+/E-/A+/A- connect to the load cell bridge.

1

DS18B20

Digital temperature sensor using OneWire protocol

4

Load Cell

Use four cells mechanically combined for a 50 kg platform scale

Four-wire strain-gauge straight-bar load cell, common 5 kg hobby scale sensor. It produces a small bridge signal and should be read through a load-cell ADC/amplifier such as HX711 or NAU7802 rather than directly by a microcontroller GPIO.

4

Load Cell

Use four cells mechanically combined for a 50 kg platform scale

Four-wire strain-gauge straight-bar load cell, common 5 kg hobby scale sensor. It produces a small bridge signal and should be read through a load-cell ADC/amplifier such as HX711 or NAU7802 rather than directly by a microcontroller GPIO.

4

Load Cell

Use four cells mechanically combined for a 50 kg platform scale

Four-wire strain-gauge straight-bar load cell, common 5 kg hobby scale sensor. It produces a small bridge signal and should be read through a load-cell ADC/amplifier such as HX711 or NAU7802 rather than directly by a microcontroller GPIO.

4

Load Cell

Use four cells mechanically combined for a 50 kg platform scale

Four-wire strain-gauge straight-bar load cell, common 5 kg hobby scale sensor. It produces a small bridge signal and should be read through a load-cell ADC/amplifier such as HX711 or NAU7802 rather than directly by a microcontroller GPIO.

4

Load Cell

Use four cells mechanically combined for a 50 kg platform scale

Four-wire strain-gauge straight-bar load cell, common 5 kg hobby scale sensor. It produces a small bridge signal and should be read through a load-cell ADC/amplifier such as HX711 or NAU7802 rather than directly by a microcontroller GPIO.

4

Load Cell

Use four cells mechanically combined for a 50 kg platform scale

Four-wire strain-gauge straight-bar load cell, common 5 kg hobby scale sensor. It produces a small bridge signal and should be read through a load-cell ADC/amplifier such as HX711 or NAU7802 rather than directly by a microcontroller GPIO.

1

Latching emergency-stop switch, 2 NC contacts

A twist-to-release red safety switch that cuts the 12 V load supply and separately signals the controller.

1

12 V isolated leak-detection relay module

A drip-tray probe module whose dry contact tells the controller when liquid has reached the tray.

1

Enclosed 12 V DC 10 A SELV DIN-rail power supply

An enclosed, certified supply that provides isolated low-voltage power for pumps, valves, drivers, and the controller buck converter.

1

Isolated 12 V to 5 V 3 A DC-DC converter

A protected converter that powers the ESP32-S3 and touchscreen from the 12 V low-voltage supply.

1

12 V 15 A blade-fuse holder with 10 A fuse

A replaceable fuse that protects the branch feeding pumps and valves from a wiring fault.

1

Opto-isolated refrigeration enable relay interface

An isolated dry-contact interface for a licensed technician to connect only to the refrigerator manufacturer's approved external-control input.

1

Food-grade 304 stainless drinking-water tank

A sealed, cleanable drinking-water reservoir with a sanitary outlet fitting.

1

Food-grade refrigerated milk tank

A removable food-grade milk reservoir that fits inside the refrigerator and can be cleaned and sanitized.

1

Food-grade 304 stainless cooking-oil tank

A sealed, cleanable cooking-oil reservoir with an oil-compatible sanitary outlet fitting.

1

HDPE liquid-detergent tank

A labeled chemical-compatible detergent reservoir kept outside the food compartment.

1

HDPE dishwashing-liquid tank

A labeled chemical-compatible dishwashing-liquid reservoir kept outside the food compartment.

1

HDPE handwash tank

A labeled chemical-compatible handwash reservoir kept outside the food compartment.

1

12 V food-grade diaphragm pump for drinking water

A self-priming drinking-water pump with food-contact wetted materials and a 12 V DC motor.

1

12 V food-grade peristaltic pump for milk

A peristaltic pump with disposable food-grade tubing so milk touches only the tube.

1

12 V food-grade gear pump for cooking oil

A food-contact gear pump sized for cooking-oil viscosity and 12 V DC operation.

1

12 V chemical-compatible diaphragm pump for detergent

A chemical-rated diaphragm pump with EPDM or Viton seals for liquid detergent.

1

12 V chemical-compatible diaphragm pump for dishwashing liquid

A chemical-rated diaphragm pump with wetted parts compatible with dishwashing liquid.

1

12 V chemical-compatible diaphragm pump for handwash

A chemical-rated diaphragm pump with wetted parts compatible with handwash liquid.

1

12 V normally-closed food-grade water solenoid valve

A food-contact valve that remains closed whenever power is removed.

1

12 V normally-closed food-grade milk pinch valve

A hygienic pinch valve that closes the dedicated milk tube without trapping milk in a valve body.

1

12 V normally-closed food-grade oil solenoid valve

An oil-compatible food-contact valve that remains closed without power.

1

12 V normally-closed chemical-rated detergent valve

A detergent-compatible valve with EPDM or Viton seals that remains closed without power.

1

12 V normally-closed chemical-rated dishwashing valve

A chemical-compatible valve that remains closed without power.

1

12 V normally-closed chemical-rated handwash valve

A chemical-compatible valve that remains closed without power.

3

Food-grade silicone dispensing tubing

Six separately labeled lengths of food-grade tubing for water, milk, and cooking oil, never shared with chemical products.

3

Chemical-compatible dispensing tubing

Three separately labeled chemical-compatible tubing runs for detergent, dishwashing liquid, and handwash.

3

Food-grade anti-drip dispensing nozzle

A sanitary dedicated outlet nozzle for a food product, kept physically away from chemical nozzles.

3

Chemical-compatible anti-drip dispensing nozzle

A dedicated chemical dispensing nozzle for one cleaning product.

1

4.7 kΩ resistor

4.7 kΩ

A small resistor that keeps the milk-temperature data wire at a valid high level.

6

Four-load-cell summing junction board

A passive junction board that combines four platform-scale load cells into one bridge signal for an HX711 amplifier.

Assemble it in 7 steps

1. Divide the cabinet into safe zones

Make three sealed physical areas: a refrigerated food area for tank_milk only, a separate food area for tank_water and tank_oil, and a lower chemical area for tank_detergent, tank_dishwash, and tank_handwash. Put a drip tray and leak_sensor_1 beneath every liquid area.

  • Never place food tanks, food tubing, or food nozzles in the detergent compartment; a spill or shared hose can contaminate food.

2. Fit the dedicated liquid paths

For each product, make one uninterrupted path: its tank, its own pump, its own valve, its own labeled tube, then its own nozzle. Use food_tubing and food_nozzle only for water, milk, and oil; use chemical_tubing and chemical_nozzle only for the three cleaning liquids. Keep milk tubing inside the refrigerator until it reaches its own insulated exit.

  • Do not use an ordinary aquarium pump, common hose, or common nozzle for milk, oil, or chemicals; the wetted material and cleaning method must be approved for that exact product.

3. Build the six weighing platforms

Mount the four load cells for each tank beneath a rigid platform, with the tank sitting only on that platform. Connect the four cells to its matching scale junction board, then connect E+, E-, A+, and A- to the matching HX711: water to hx_water, milk to hx_milk, oil to hx_oil, detergent to hx_detergent, dishwash to hx_dishwash, and handwash to hx_handwash.

  • Leave a small service loop in each load-cell cable so it cannot pull on the platform and change the measured weight.

4. Wire the low-voltage control box

Mount psu_12v, main_fuse, estop_1, buck_5v, pump_driver_1, valve_driver_1, and the ESP32-S3 in a closed non-food control box. Wire the fused 12 V path in this order: PSU positive to fuse, fuse to the emergency-stop load contact, then emergency-stop output to every pump and valve positive lead and both driver V+ terminals. Each pump and valve negative lead goes only to its numbered driver output.

  • The emergency-stop switch must physically remove 12 V from every pump and valve; software alone is not a safety device.
  • Mains wiring, protective earth, and the enclosed power supply must be installed and checked by a qualified electrician.

5. Connect the ESP32-S3 signals

Connect touchscreen_1 as follows: VCC to 3V3 (power), GND to GND (ground), MOSI to GPIO11 (data), MISO to GPIO13 (data), SCK to GPIO12 (clock), TFT_CS to GPIO10 (screen select), TFT_DC to GPIO21 (screen command/data), TFT_RST to GPIO38 (screen reset), and TOUCH_CS to GPIO39 (touch select). Connect both driver boards SDA to GPIO8 (data) and SCL to GPIO9 (clock); set the pump board to address 0x20 and valve board to 0x21.

  • Use only 3.3 V logic at the ESP32-S3 pins; 5 V on a GPIO can damage the board.

6. Connect safety and milk monitoring

Put milk_temp in a sealed food-safe thermowell in the milk reservoir or refrigerator air path. Wire its VCC to 3V3 (power), GND to GND (ground), DATA to GPIO14 (signal), and place onewire_pullup between 3V3 and that DATA wire. Wire the emergency-stop signal contact to GPIO15 and ground, and the leak detector contact to GPIO16 and ground. Connect fridge_interface only to the refrigerator manufacturer-approved external control terminals.

  • Do not splice into a refrigerator compressor or mains thermostat circuit; a refrigeration technician must approve and make that connection.

7. Test and calibrate before filling

With all tanks empty and no mains connected to the refrigerator interface, power only the low-voltage system. Confirm the emergency stop and leak probe immediately turn all outputs off. Tare each empty platform, then place a known test weight on it and replace the matching calibration value in the firmware. Test one pump and valve at a time with water; measure delivered mass and set density-correct target grams for oil and cleaning liquids before live vending.

  • Never test a chemical channel with a food nozzle or food collection vessel.
  • Do not enable sales until the backend returns a verified payment and all scale, temperature, leak, and emergency-stop tests pass.

Review all connections

1. Connections between "touchscreen_1" and "ESP32"

Functiontouchscreen_1ESP32
powerVCC → 3.3 V power3V3
groundGND → groundGND
spiMOSI → SPI display dataGPIO 11
spiMISO → SPI touch dataGPIO 13
spiSCK → SPI clockGPIO 12
digitalTFT_CS → display selectGPIO 10
digitalTFT_DC → display command/dataGPIO 21
digitalTFT_RST → display resetGPIO 38
digitalTOUCH_CS → touch controller selectGPIO 39

2. Connections between "pump_driver_1" and "ESP32"

Functionpump_driver_1ESP32
powerVCC → 3.3 V logic power3V3
groundGND → common groundGND
i2cSDA → I2C data, address 0x20GPIO 8
i2cSCL → I2C clockGPIO 9
powerV+ → Latching emergency-stop switch, 2 NC contacts NC_LOAD_OUTEXT
digitalOUT0 → 12 V food-grade diaphragm pump for drinking water GNDEXT
digitalOUT1 → 12 V food-grade peristaltic pump for milk GNDEXT
digitalOUT2 → 12 V food-grade gear pump for cooking oil GNDEXT
digitalOUT3 → 12 V chemical-compatible diaphragm pump for detergent GNDEXT
digitalOUT4 → 12 V chemical-compatible diaphragm pump for dishwashing liquid GNDEXT
digitalOUT5 → 12 V chemical-compatible diaphragm pump for handwash GNDEXT

3. Connections between "valve_driver_1" and "ESP32"

Functionvalve_driver_1ESP32
powerVCC → 3.3 V logic power3V3
groundGND → common groundGND
i2cSDA → I2C data, address 0x21GPIO 8
i2cSCL → I2C clockGPIO 9
powerV+ → Latching emergency-stop switch, 2 NC contacts NC_LOAD_OUTEXT
digitalOUT0 → 12 V normally-closed food-grade water solenoid valve GNDEXT
digitalOUT1 → 12 V normally-closed food-grade milk pinch valve GNDEXT
digitalOUT2 → 12 V normally-closed food-grade oil solenoid valve GNDEXT
digitalOUT3 → 12 V normally-closed chemical-rated detergent valve GNDEXT
digitalOUT4 → 12 V normally-closed chemical-rated dishwashing valve GNDEXT
digitalOUT5 → 12 V normally-closed chemical-rated handwash valve GNDEXT

4. Connections between "hx_water" and "ESP32"

Functionhx_waterESP32
powerVCC → 3.3 V scale power3V3
groundGND → groundGND
digitalDT → water scale dataGPIO 1
digitalSCK → water scale clockGPIO 18

5. Connections between "hx_milk" and "ESP32"

Functionhx_milkESP32
powerVCC → 3.3 V scale power3V3
groundGND → groundGND
digitalDT → milk scale dataGPIO 2
digitalSCK → milk scale clockGPIO 40

6. Connections between "hx_oil" and "ESP32"

Functionhx_oilESP32
powerVCC → 3.3 V scale power3V3
groundGND → groundGND
digitalDT → oil scale dataGPIO 3
digitalSCK → oil scale clockGPIO 41

7. Connections between "hx_detergent" and "ESP32"

Functionhx_detergentESP32
powerVCC → 3.3 V scale power3V3
groundGND → groundGND
digitalDT → detergent scale dataGPIO 4
digitalSCK → detergent scale clockGPIO 42

8. Connections between "hx_dishwash" and "ESP32"

Functionhx_dishwashESP32
powerVCC → 3.3 V scale power3V3
groundGND → groundGND
digitalDT → dishwash scale dataGPIO 5
digitalSCK → dishwash scale clockGPIO 47

9. Connections between "hx_handwash" and "ESP32"

Functionhx_handwashESP32
powerVCC → 3.3 V scale power3V3
groundGND → groundGND
digitalDT → handwash scale dataGPIO 6
digitalSCK → HX711 Load Cell Amplifier SCKEXT

10. Connections between "milk_temp" and "ESP32"

Functionmilk_tempESP32
powerVCC → 3.3 V power3V3
groundGND → groundGND
dataDATA → milk temperature signalGPIO 14

11. Connections between "onewire_pullup" and "ESP32"

Functiononewire_pullupESP32
powerA → pull-up supply3V3
digitalB → DS18B20 DATAEXT

12. Connections between "estop_1" and "ESP32"

Functionestop_1ESP32
powerNC_LOAD_IN → 12 V 15 A blade-fuse holder with 10 A fuse OUTEXT
digitalNC_SIGNAL → emergency-stop state inputGPIO 15
groundCOM_SIGNAL → signal groundGND
powerNC_LOAD_OUT → 12 V food-grade diaphragm pump for drinking water +12VEXT
powerNC_LOAD_OUT → 12 V food-grade peristaltic pump for milk +12VEXT
powerNC_LOAD_OUT → 12 V food-grade gear pump for cooking oil +12VEXT
powerNC_LOAD_OUT → 12 V chemical-compatible diaphragm pump for detergent +12VEXT
powerNC_LOAD_OUT → 12 V chemical-compatible diaphragm pump for dishwashing liquid +12VEXT
powerNC_LOAD_OUT → 12 V chemical-compatible diaphragm pump for handwash +12VEXT
powerNC_LOAD_OUT → 12 V normally-closed food-grade water solenoid valve +12VEXT
powerNC_LOAD_OUT → 12 V normally-closed food-grade milk pinch valve +12VEXT
powerNC_LOAD_OUT → 12 V normally-closed food-grade oil solenoid valve +12VEXT
powerNC_LOAD_OUT → 12 V normally-closed chemical-rated detergent valve +12VEXT
powerNC_LOAD_OUT → 12 V normally-closed chemical-rated dishwashing valve +12VEXT
powerNC_LOAD_OUT → 12 V normally-closed chemical-rated handwash valve +12VEXT

13. Connections between "leak_sensor_1" and "ESP32"

Functionleak_sensor_1ESP32
powerV+ → Latching emergency-stop switch, 2 NC contacts NC_LOAD_OUTEXT
groundGND → groundGND
digitalDRY_CONTACT → leak alarm inputGPIO 16
groundCOM → contact groundGND

14. Connections between "fridge_interface" and "ESP32"

Functionfridge_interfaceESP32
powerVCC → isolated control logic power3V3
groundGND → control groundGND
digitalIN → refrigeration enable requestGPIO 17
digitalNO → refrigerator manufacturer-approved external-control input, installed by refrigeration technicianEXT
digitalCOM → refrigerator manufacturer-approved external-control common, installed by refrigeration technicianEXT

15. Connections between "psu_12v" and "ESP32"

Functionpsu_12vESP32
powerAC_N → qualified electrician-installed mains neutralEXT
groundPE → protective earth bonded to metal enclosureEXT
powerV+ → 12 V 15 A blade-fuse holder with 10 A fuse INEXT
groundV- → 12 V return/common groundGND
powerAC_L → locked service-enclosure mains live, installed by a qualified electricianEXT

16. Connections between "buck_5v" and "ESP32"

Functionbuck_5vESP32
powerVIN+ → Latching emergency-stop switch, 2 NC contacts NC_LOAD_OUTEXT
groundVIN- → groundGND
powerVOUT+ → 5 V to ESP32-S3 VINVIN
groundVOUT- → groundGND

17. Connections between "scale_junctions" and "ESP32"

Functionscale_junctionsESP32
powerE+ → HX711 Load Cell Amplifier E+EXT
groundE- → HX711 Load Cell Amplifier E-EXT
analogA+ → HX711 Load Cell Amplifier A+EXT
analogA- → HX711 Load Cell Amplifier A-EXT

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <XPT2046_Touchscreen.h>
#include <HX711.h>
#include <OneWire.h>
#include <DallasTemperature.h>

constexpr int I2C_SDA = 8, I2C_SCL = 9;
constexpr int TFT_CS = 10, TFT_DC = 21, TFT_RST = 38, TOUCH_CS = 39;
constexpr int HX_WATER_DT = 1, HX_MILK_DT = 2, HX_OIL_DT = 3, HX_DET_DT = 4, HX_DISH_DT = 5, HX_HAND_DT = 6;
constexpr int HX_WATER_SCK = 18, HX_MILK_SCK = 40, HX_OIL_SCK = 41, HX_DET_SCK = 42, HX_DISH_SCK = 47, HX_HAND_SCK = 18; // shared electrical clock net with water HX711
constexpr int MILK_TEMP_PIN = 14, ESTOP_PIN = 15, LEAK_PIN = 16, FRIDGE_ENABLE_PIN = 17;
constexpr uint8_t PUMP_DRIVER = 0x20, VALVE_DRIVER = 0x21;
constexpr float MILK_MAX_C = 4.0f, LOW_STOCK_G = 1000.0f;
constexpr char WIFI_SSID[] = "";          // provision during commissioning
constexpr char WIFI_PASSWORD[] = "";
constexpr char BACKEND_URL[] = "";        // e.g. https://your-server.example
constexpr char DEVICE_TOKEN[] = "";       // short-lived device credential, never an M-Pesa secret

Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST);
XPT2046_Touchscreen touch(TOUCH_CS);
OneWire oneWire(MILK_TEMP_PIN);
DallasTemperature milkThermometer(&oneWire);
HX711 scales[6];
const int dtPins[6] = {HX_WATER_DT,HX_MILK_DT,HX_OIL_DT,HX_DET_DT,HX_DISH_DT,HX_HAND_DT};
const int sckPins[6] = {HX_WATER_SCK,HX_MILK_SCK,HX_OIL_SCK,HX_DET_SCK,HX_DISH_SCK,HX_HAND_SCK};
const char *names[6] = {"Water","Milk","Cooking oil","Detergent","Dishwash","Handwash"};
// Replace each factor during the calibration procedure; units are grams per displayed value.
float calibration[6] = {1,1,1,1,1,1};
float stockG[6] = {0,0,0,0,0,0};
int selectedProduct = 0;
int selectedQtyMl = 500;
float milkC = NAN;
bool dispensing = false;
uint8_t pumpBits = 0, valveBits = 0;
uint32_t lastUiMs = 0, lastSensorMs = 0;

void mcpWrite(uint8_t address, uint8_t reg, uint8_t value) {
  Wire.beginTransmission(address); Wire.write(reg); Wire.write(value); Wire.endTransmission();
}
void setOutputs(uint8_t &bits, uint8_t address, uint8_t newBits) {
  bits = newBits; mcpWrite(address, 0x00, 0x00); mcpWrite(address, 0x14, bits);
}
void allOff() { setOutputs(pumpBits, PUMP_DRIVER, 0); setOutputs(valveBits, VALVE_DRIVER, 0); dispensing = false; }
bool emergencyActive() { return digitalRead(ESTOP_PIN) == HIGH; } // NC contact opens when pressed
bool leakActive() { return digitalRead(LEAK_PIN) == LOW; }       // dry contact closes when wet
bool milkSafe() { return !isnan(milkC) && milkC >= 0.0f && milkC <= MILK_MAX_C; }
bool safeToDispense(int product) {
  return !emergencyActive() && !leakActive() && (product != 1 || milkSafe()) && stockG[product] > LOW_STOCK_G;
}

void drawUi() {
  tft.fillScreen(ILI9341_BLACK);
  tft.setTextSize(2); tft.setTextColor(ILI9341_WHITE); tft.setCursor(8, 6); tft.print("Household Refill");
  tft.setTextSize(1); tft.setCursor(8, 28); tft.print("Milk: "); tft.print(milkC, 1); tft.print(" C  ");
  if (emergencyActive()) tft.setTextColor(ILI9341_RED); else if (leakActive()) tft.setTextColor(ILI9341_RED); else tft.setTextColor(ILI9341_GREEN);
  if (emergencyActive()) tft.print("EMERGENCY STOP"); else if (leakActive()) tft.print("LEAK LOCKOUT"); else tft.print("SAFE");
  for (int i=0;i<6;i++) {
    int y = 45 + i*28; uint16_t color = (i == selectedProduct) ? ILI9341_BLUE : ILI9341_DARKGREY;
    tft.fillRect(4,y,210,24,color); tft.setTextColor(ILI9341_WHITE); tft.setCursor(8,y+7); tft.print(names[i]);
    tft.setCursor(135,y+7); tft.print(stockG[i]/1000.0f,1); tft.print("kg");
  }
  tft.fillRect(220,45,96,38,ILI9341_DARKCYAN); tft.setTextColor(ILI9341_WHITE); tft.setCursor(228,55); tft.print("-100 ml");
  tft.fillRect(220,90,96,38,ILI9341_DARKCYAN); tft.setCursor(228,100); tft.print("+100 ml");
  tft.fillRect(220,140,96,42,ILI9341_GREEN); tft.setTextColor(ILI9341_BLACK); tft.setCursor(232,151); tft.print("PAY");
  tft.setTextColor(ILI9341_WHITE); tft.setCursor(224,195); tft.print(selectedQtyMl); tft.print(" ml");
  tft.setCursor(224,215); tft.print("Backend verifies"); tft.setCursor(224,226); tft.print("M-Pesa payment");
}

bool backendPaymentConfirmed(int product, int quantityMl) {
  // The backend owns M-Pesa consumer key, secret, OAuth token and STK callback verification.
  // An empty URL deliberately denies dispensing until commissioning has configured a backend.
  if (strlen(BACKEND_URL) == 0 || WiFi.status() != WL_CONNECTED) return false;
  WiFiClientSecure client; client.setInsecure(); // commissioning only: replace with backend CA validation before production
  HTTPClient http;
  String url = String(BACKEND_URL) + "/api/v1/vend/authorize";
  if (!http.begin(client, url)) return false;
  http.addHeader("Content-Type", "application/json"); http.addHeader("Authorization", String("Bearer ") + DEVICE_TOKEN);
  String body = String("{\"product\":\"") + names[product] + "\",\"quantity_ml\":" + quantityMl + "}";
  int status = http.POST(body); String reply = http.getString(); http.end();
  return status == 200 && reply.indexOf("\"verified\":true") >= 0;
}

void dispenseByMass(int product, int quantityMl) {
  // For this prototype 1 ml is treated as 1 g. During product calibration, use density-correct target grams for oil/detergents.
  float targetG = (float)quantityMl;
  float startG = stockG[product];
  if (!safeToDispense(product)) return;
  setOutputs(valveBits, VALVE_DRIVER, valveBits | (1 << product));
  setOutputs(pumpBits, PUMP_DRIVER, pumpBits | (1 << product));
  dispensing = true; uint32_t started = millis();
  while (dispensing && millis() - started < 120000UL) {
    if (!safeToDispense(product)) break;
    if (scales[product].is_ready()) stockG[product] = scales[product].get_units(3);
    if (startG - stockG[product] >= targetG) break;
    delay(20);
  }
  allOff();
}

void readSensors() {
  milkThermometer.requestTemperatures(); milkC = milkThermometer.getTempCByIndex(0);
  for (int i=0;i<6;i++) if (scales[i].is_ready()) stockG[i] = scales[i].get_units(3);
  // Closed contact requests refrigeration only while milk is too warm; technician must connect only to approved appliance input.
  digitalWrite(FRIDGE_ENABLE_PIN, (!isnan(milkC) && milkC > 3.0f) ? HIGH : LOW);
  if (emergencyActive() || leakActive() || !milkSafe()) allOff();
}

void handleTouch() {
  if (!touch.touched()) return;
  TS_Point p = touch.getPoint();
  // Calibrate these raw ranges after fitting the physical display.
  int x = map(p.x, 200, 3800, 0, 320); int y = map(p.y, 200, 3800, 0, 240);
  if (x < 215 && y >= 45 && y < 213) selectedProduct = constrain((y - 45) / 28, 0, 5);
  else if (x >= 220 && y >= 45 && y < 83) selectedQtyMl = max(100, selectedQtyMl - 100);
  else if (x >= 220 && y >= 90 && y < 128) selectedQtyMl = min(5000, selectedQtyMl + 100);
  else if (x >= 220 && y >= 140 && y < 182) {
    if (backendPaymentConfirmed(selectedProduct, selectedQtyMl)) dispenseByMass(selectedProduct, selectedQtyMl);
  }
  delay(180); drawUi();
}

void setup() {
  pinMode(ESTOP_PIN, INPUT_PULLUP); pinMode(LEAK_PIN, INPUT_PULLUP); pinMode(FRIDGE_ENABLE_PIN, OUTPUT); digitalWrite(FRIDGE_ENABLE_PIN, LOW);
  Wire.begin(I2C_SDA, I2C_SCL); allOff();
  for (int i=0;i<6;i++) { scales[i].begin(dtPins[i], sckPins[i]); scales[i].set_scale(calibration[i]); scales[i].tare(); }
  milkThermometer.begin();
  SPI.begin(12,13,11,10); tft.begin(); tft.setRotation(1); touch.begin(); touch.setRotation(1);
  if (strlen(WIFI_SSID)) { WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); }
  readSensors(); drawUi();
}
void loop() {
  uint32_t now = millis();
  if (now - lastSensorMs >= 1000) { lastSensorMs = now; readSensors(); }
  if (now - lastUiMs >= 1000) { lastUiMs = now; drawUi(); }
  handleTouch();
  delay(10);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik