Community project

AI Nutritional Smart Scale

ESP32
Photo of AI Nutritional Smart Scale
Generated with AI

Devi Kiran Reddy Kesavarapu

Published October 19, 2026

This project combines an ESP32-CAM with a precision load cell to create a smart scale that weighs food and captures images for nutritional analysis. The scale uses an HX711 amplifier to convert load cell readings into accurate weight measurements, while the built-in OV2640 camera provides visual data for food identification. The guide includes a complete wiring diagram, parts list, calibration procedure, and firmware to get the scale operational.

Builders will learn how to integrate analog weight sensing with computer vision on a microcontroller, set up a local web interface for the scale, and implement food recognition with nutritional lookup tables. The assembly process covers mounting the load cell, connecting the HX711 amplifier, configuring camera pins, and performing the critical tare and calibration steps needed for accurate measurements.

Wiring diagram

Gather all the parts

QtyComponent
1

Load Cell

5 kg

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

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.

Assemble it in 5 steps

1. Mount the weighing bar

Screw one end of the load cell's metal bar firmly to a rigid base and screw the other end to a small platform. The platform must not touch the base anywhere else, because the bar needs to bend slightly under the food.

  • Put the food in the middle of the platform for the most repeatable readings.
  • Do not bend the load cell by hand or load it beyond 5 kg; permanent bending makes weight readings wrong.

2. Connect the load cell to its amplifier

Join the four load-cell wires to the matching labeled terminals on the HX711: E+ to E+, E- to E-, A+ to A+, and A- to A-. These wires carry the tiny measuring signal from the metal bar to the amplifier.

  • Use the labels printed on the HX711 board rather than wire colours, because load-cell wire colours vary by maker.
  • Do not connect the load cell directly to an ESP32-CAM pin; it needs the HX711 amplifier.

3. Power and connect the HX711

Connect HX711 VCC to the ESP32-CAM 3V3 pin (power), HX711 GND to ESP32-CAM GND (ground), HX711 DT to GPIO13 (weight data), and HX711 SCK to GPIO14 (clock signal). Do not fit a microSD card while these two pins are used for the scale.

  • Keep the four wires between the load cell and HX711 away from the camera and USB power wires where possible.
  • Make sure VCC and GND are not swapped — swapped power can damage the HX711.

4. Position the camera

Aim the ESP32-CAM's built-in camera at the centre of the platform from above or at an angle where it sees the food. Add steady, even room lighting so food colour is not strongly changed by shadows.

  • Keep the camera view and lighting similar whenever you use the food classifier.
  • Keep all electronics away from spills; food or liquid on the board can short its connections.

5. Tare and calibrate the scale

With the empty platform installed, power the ESP32-CAM from USB. On your phone or tablet, join the Wi-Fi network named SmartScale-AI using password weigh-food, then open 192.168.4.1. Tap Tare empty scale, place a known weight on the platform, enter its grams, and tap Calibrate.

  • A sealed 500 g or 1000 g kitchen weight gives better calibration than guessing an object's mass.
  • Remove the known weight after calibration before weighing food.

Review all connections

1. Connections between "hx711_1" and "ESP32"

Functionhx711_1ESP32
powerVCC3V3
groundGNDGND
digitalDTGPIO 13
digitalSCKGPIO 14
powerE+ → Load Cell E+EXT
groundE- → Load Cell E-EXT
analogA+ → Load Cell A+EXT
analogA- → Load Cell A-EXT

Deploy the firmware

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <HX711.h>
#include "esp_camera.h"

// HX711 wiring: DT -> GPIO13, SCK -> GPIO14.
#define HX711_DT_PIN 13
#define HX711_SCK_PIN 14

// AI Thinker ESP32-CAM's built-in OV2640 camera wiring.
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

const char *AP_SSID = "SmartScale-AI";
const char *AP_PASSWORD = "weigh-food";
const float DEFAULT_CALIBRATION = 420.0f;
const uint8_t TREE_COUNT = 64;

HX711 scale;
WebServer server(80);
Preferences preferences;
float calibrationFactor = DEFAULT_CALIBRATION;
float weightG = 0.0f;
uint32_t lastWeightMs = 0;
uint32_t lastVisionMs = 0;
uint8_t avgR = 0, avgG = 0, avgB = 0;
String foodName = "Scanning";
uint8_t confidence = 0;

const char *foodNames[] = {"Apple", "Banana", "Orange", "Tomato", "Unknown"};
const float kcal100[] = {52.0f, 89.0f, 47.0f, 18.0f, 0.0f};
const float carb100[] = {13.8f, 22.8f, 11.8f, 3.9f, 0.0f};
const float protein100[] = {0.3f, 1.1f, 0.9f, 0.9f, 0.0f};
const float fat100[] = {0.2f, 0.3f, 0.1f, 0.2f, 0.0f};

const char PAGE[] PROGMEM = R"HTML(
<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Smart Scale AI</title><style>
*{box-sizing:border-box}body{margin:0;background:#071a19;color:#ecfff4;font-family:Arial,sans-serif;min-height:100vh;background:radial-gradient(circle at top,#125a49,#071a19 62%)}main{max-width:760px;margin:auto;padding:20px}.tag{display:inline-block;background:#d8f447;color:#172400;padding:6px 10px;border-radius:20px;font-size:12px;font-weight:bold;letter-spacing:1px}h1{margin:12px 0 4px;font-size:30px}.sub{color:#a2c5bc;margin:0 0 18px}.grid{display:grid;grid-template-columns:1.2fr .8fr;gap:14px}.card{background:rgba(13,43,40,.9);border:1px solid #28796a;border-radius:20px;padding:18px;box-shadow:0 10px 30px #00100d}.label{color:#9fc8bd;font-size:12px;text-transform:uppercase;letter-spacing:1px}.weight{font-size:62px;font-weight:800;line-height:1;margin-top:8px}.unit{font-size:19px;color:#d8f447}.food{font-size:32px;font-weight:bold;margin:9px 0}.confidence{font-size:26px;color:#d8f447;margin:7px 0}.bar{height:10px;background:#183e38;border-radius:8px;overflow:hidden}.bar i{display:block;height:100%;background:linear-gradient(90deg,#d8f447,#52e6a6);width:0}.vision{font-size:13px;color:#9fc8bd;margin-top:12px}.nutrition{margin-top:14px}.nut{display:flex;justify-content:space-between;border-bottom:1px solid #24574e;padding:10px 0}.nut b{color:#d8f447}.controls{margin-top:14px;display:flex;gap:9px;flex-wrap:wrap}button,input{border-radius:10px;padding:11px 13px;font-size:15px;border:1px solid #4b9c88}button{background:#d8f447;color:#142400;font-weight:bold}input{background:#102f2b;color:#fff;width:115px}.hint{font-size:12px;color:#a2c5bc;margin-top:14px;line-height:1.45}@media(max-width:560px){.grid{grid-template-columns:1fr}.weight{font-size:54px}}
</style></head><body><main><span class="tag">ESP32-CAM • 64 TREE CLASSIFIER</span><h1>AI Smart Scale</h1><p class="sub">Live food estimate and nutrition</p><div class="grid"><section class="card"><div class="label">Live weight</div><div class="weight"><span id="w">0</span> <span class="unit">g</span></div><div class="controls"><button onclick="tare()">Tare empty scale</button><input id="known" type="number" placeholder="known g"><button onclick="cal()">Calibrate</button></div><div class="hint">First use: remove everything, tap Tare, then place a known weight and enter its grams before tapping Calibrate.</div></section><section class="card"><div class="label">AI result</div><div class="food" id="food">Scanning</div><div class="confidence"><span id="conf">0</span>% confidence</div><div class="bar"><i id="bar"></i></div><div class="vision">Camera colour sample: <span id="rgb">—</span></div></section></div><section class="card nutrition"><div class="label">Estimated nutrition for this weight</div><div class="nut"><span>Energy</span><b><span id="kcal">0</span> kcal</b></div><div class="nut"><span>Carbohydrate</span><b><span id="carb">0</span> g</b></div><div class="nut"><span>Protein</span><b><span id="protein">0</span> g</b></div><div class="nut"><span>Fat</span><b><span id="fat">0</span> g</b></div></section></main><script>
async function refresh(){let d=await (await fetch('/data')).json();for(let k of ['w','food','conf','kcal','carb','protein','fat','rgb'])document.getElementById(k).textContent=d[k];document.getElementById('bar').style.width=d.conf+'%'}
async function tare(){await fetch('/tare');setTimeout(refresh,400)}async function cal(){let g=document.getElementById('known').value;if(g>0){await fetch('/calibrate?g='+g);setTimeout(refresh,600)}}refresh();setInterval(refresh,750);
</script></body></html>)HTML";

void initCamera() {
  camera_config_t c = {};
  c.ledc_channel = LEDC_CHANNEL_0; c.ledc_timer = LEDC_TIMER_0;
  c.pin_d0 = Y2_GPIO_NUM; c.pin_d1 = Y3_GPIO_NUM; c.pin_d2 = Y4_GPIO_NUM; c.pin_d3 = Y5_GPIO_NUM;
  c.pin_d4 = Y6_GPIO_NUM; c.pin_d5 = Y7_GPIO_NUM; c.pin_d6 = Y8_GPIO_NUM; c.pin_d7 = Y9_GPIO_NUM;
  c.pin_xclk = XCLK_GPIO_NUM; c.pin_pclk = PCLK_GPIO_NUM; c.pin_vsync = VSYNC_GPIO_NUM; c.pin_href = HREF_GPIO_NUM;
  c.pin_sccb_sda = SIOD_GPIO_NUM; c.pin_sccb_scl = SIOC_GPIO_NUM; c.pin_pwdn = PWDN_GPIO_NUM; c.pin_reset = RESET_GPIO_NUM;
  c.xclk_freq_hz = 20000000; c.pixel_format = PIXFORMAT_RGB565; c.frame_size = FRAMESIZE_QQVGA;
  c.jpeg_quality = 12; c.fb_count = 1;
  if (esp_camera_init(&c) != ESP_OK) { foodName = "Camera error"; }
}

void sampleCamera() {
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb || fb->format != PIXFORMAT_RGB565) { if (fb) esp_camera_fb_return(fb); return; }
  uint32_t sr = 0, sg = 0, sb = 0, n = 0;
  for (size_t i = 0; i + 1 < fb->len; i += 160) {
    uint16_t px = ((uint16_t)fb->buf[i] << 8) | fb->buf[i + 1];
    sr += ((px >> 11) & 0x1F) << 3; sg += ((px >> 5) & 0x3F) << 2; sb += (px & 0x1F) << 3; n++;
  }
  esp_camera_fb_return(fb);
  if (n) { avgR = sr / n; avgG = sg / n; avgB = sb / n; }
}

uint8_t runForest() {
  if (weightG < 3.0f) { confidence = 0; foodName = "No food detected"; return 4; }
  uint8_t votes[5] = {0, 0, 0, 0, 0};
  for (uint8_t t = 0; t < TREE_COUNT; t++) {
    // Each index is a small decision tree with slightly shifted colour/weight splits.
    int apple = avgR > (avgG + 7 - (t % 5)) && avgG > avgB + (t % 6) && weightG < 1200;
    int banana = avgR > avgB + 12 && avgG > avgB + 10 && avgG > 68 + (t % 9) && weightG < 900;
    int orange = avgR > avgG + (8 - (t % 5)) && avgG > avgB + 4 && weightG < 1000;
    int tomato = avgR > avgG + 10 && avgR > avgB + 17 && weightG < 1000;
    uint8_t pick = 4;
    switch (t % 4) { case 0: if (apple) pick=0; else if (tomato) pick=3; else if (orange) pick=2; else if (banana) pick=1; break;
      case 1: if (banana) pick=1; else if (apple) pick=0; else if (orange) pick=2; else if (tomato) pick=3; break;
      case 2: if (orange) pick=2; else if (tomato) pick=3; else if (apple) pick=0; else if (banana) pick=1; break;
      default: if (tomato) pick=3; else if (apple) pick=0; else if (banana) pick=1; else if (orange) pick=2; }
    votes[pick]++;
  }
  uint8_t best = 0; for (uint8_t i = 1; i < 5; i++) if (votes[i] > votes[best]) best = i;
  confidence = (uint8_t)((100UL * votes[best]) / TREE_COUNT); foodName = foodNames[best]; return best;
}

String jsonData() {
  uint8_t kind = runForest(); float portion = weightG / 100.0f;
  String s = "{\"w\":\"" + String(weightG, 1) + "\",\"food\":\"" + foodName + "\",\"conf\":\"" + String(confidence) + "\",\"rgb\":\"R" + String(avgR) + " G" + String(avgG) + " B" + String(avgB) + "\",\"kcal\":\"" + String(kcal100[kind] * portion, 1) + "\",\"carb\":\"" + String(carb100[kind] * portion, 1) + "\",\"protein\":\"" + String(protein100[kind] * portion, 1) + "\",\"fat\":\"" + String(fat100[kind] * portion, 1) + "\"}"; return s;
}

void setup() {
  Serial.begin(115200); scale.begin(HX711_DT_PIN, HX711_SCK_PIN);
  preferences.begin("smartscale", false); calibrationFactor = preferences.getFloat("cal", DEFAULT_CALIBRATION); scale.set_scale(calibrationFactor); scale.tare(15);
  initCamera(); WiFi.mode(WIFI_AP); WiFi.softAP(AP_SSID, AP_PASSWORD);
  server.on("/", [](){ server.send_P(200, "text/html", PAGE); });
  server.on("/data", [](){ server.send(200, "application/json", jsonData()); });
  server.on("/tare", [](){ scale.tare(15); server.send(200, "text/plain", "Tared"); });
  server.on("/calibrate", [](){ if (!server.hasArg("g") || server.arg("g").toFloat() <= 0) { server.send(400, "text/plain", "Enter known grams"); return; } scale.set_scale(1.0f); float raw = scale.get_value(20); calibrationFactor = raw / server.arg("g").toFloat(); scale.set_scale(calibrationFactor); preferences.putFloat("cal", calibrationFactor); server.send(200, "text/plain", "Calibrated"); });
  server.begin();
}

void loop() {
  server.handleClient(); uint32_t now = millis();
  if (now - lastWeightMs >= 350 && scale.is_ready()) { lastWeightMs = now; weightG = scale.get_units(8); if (weightG < 0.0f && weightG > -3.0f) weightG = 0.0f; }
  if (now - lastVisionMs >= 2200) { lastVisionMs = now; sampleCamera(); }
  delay(5);
}

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