← Notes

Building a BLE kitchen scale from a load cell

6 min read

ScalePlate is a food-logging app built around one number: the exact weight of what you’re about to eat. The app can recognise a product and pull its nutrition, but “how much” has to come from real hardware. So instead of leaning on an off-the-shelf smart scale with a closed protocol, I built the scale myself — foodrec-tenso, an ESP32-C3 and an HX711 turning a load cell into grams and streaming them over Bluetooth LE. This note is the log of that build: the decisions, the protocol, and the things the hardware taught me.

Side view of the scale: two bamboo boards with a metal load-cell beam and a green HX711 board with coloured wires visible in the gap between them
The whole scale: a load-cell beam and an HX711 sandwiched between two bamboo boards.

Why build the scale at all

The whole project rests on a clean split of responsibilities: weight comes from hardware, identity comes from a barcode or a vision model, and nutrition comes from a food database. The final record is just weight × nutrients per 100 g. That means the scale has exactly one job — report trustworthy grams — and it should do it well enough that the phone can stay a thin client.

Commercial BLE scales exist, but their protocols are undocumented or coarse, and I wanted the weight pipeline to be something I fully control and can calibrate. A load cell, a 24-bit ADC, and a microcontroller with BLE cost very little and give you a device that speaks exactly the protocol you design.

The hardware

  • Load cell — a single-point 5 kg beam. An aluminium bar with four wires, which means it’s already a full Wheatstone bridge and wires straight into the HX711’s channel A. The 5 kg range is deliberate: the maximum is the heaviest food plus a heavy pot (1–1.5 kg), and full scale should sit around 1.5–2× that. After filtering it gives a stable ~0.5–1 g — enough for every kitchen portion except micro-doses of spices.
  • ADC — HX711. A 24-bit converter dedicated to bridge sensors, running on channel A at gain 128, sampling at 10 SPS (quieter and more precise than 80 SPS, which is plenty for food).
  • MCU — ESP32-C3 Super Mini. Cheap, has BLE, and enough GPIO once you avoid the strapping and USB pins.
Macro of a green HX711 breakout board labelled XFW-HX711, with bridge inputs E+, E−, A−, A+, B−, B+ on one side and GND, DT, SCK, VCC plus 10Hz/80Hz rate jumpers on the other
HX711 breakout — bridge inputs on the left, DT/SCK/power on the right, and the 10 Hz / 80 Hz rate jumper up top.
Macro of a small black ESP32-C3 Super Mini board with a USB-C port and RST, BOOT and 5V pin labels, wired to a harness of coloured jumper wires
ESP32-C3 Super Mini, wired to the HX711 — USB-C, RST/BOOT, and the GPIO harness.

One power detail matters: the HX711 is powered from 3.3 V, not 5 V. It runs fine at 5 V, but then its data line would swing to 5 V into a 3.3 V input. Matching the supply to the MCU’s logic level avoids level shifters entirely.

Weight lives in the firmware, not the client

The single most important architectural decision: the ESP32 owns the raw data and does all the maths — averaging, stability detection, tare, and the calibration factor. It hands out grams already computed, so any client (Mac, phone, a debugging BLE scanner) sees the same number.

The reason is that the calibration factor and the zero offset are properties of one specific load cell. They have to survive a reconnect and a change of client, so they belong in non-volatile storage on the ESP32, not in the app. The measurement loop is straightforward:

  1. Read the 24-bit raw value from the HX711.
  2. Filter it (moving average / median over a window) to kill outliers.
  3. Subtract the tare offset (captured on command, stored in NVS).
  4. Divide by the calibration factor — (raw with reference − tare) / known grams, also stored in NVS.
  5. Mark the reading stable when its spread over ~1 s falls under a threshold.

That stable flag is the quiet hero: a diary entry is only recorded off a stable weight, so the app never captures the moment mid-pour while food is still landing on the plate.

The BLE protocol

The Bluetooth SIG has a standard Weight Scale profile (0x181D), but it’s built for bathroom scales — its fixed format quantises to 5 g steps, far too coarse for food. So the main weight characteristic is custom:

Characteristic Properties Payload
Weight (custom UUID) Notify int32 LE weight in milligrams, plus a flags byte (bit 0 = stable, bit 1 = just tared). 5 bytes.
Control (custom UUID) Write uint8 opcode (+ uint32 argument for calibration): tare, calibrate, save.

Two choices worth calling out. Milligrams over the air — an int32 in milligrams keeps sub-gram resolution with headroom and avoids sending floats over BLE; the Swift side divides by 1000 once, at the boundary, and everything above it is grams. And the device advertises its custom service UUID, so the client can scan by UUID and find its own scale without walking every Bluetooth device in the room.

What’s done, and what the hardware taught me

The firmware side is complete through the BLE server — raw read, calibration in NVS, the stability flag, and a NimBLE GATT server that a generic scanner (nRF Connect, LightBlue) can subscribe to and command. Keeping the firmware verifiable with a third-party scanner meant it could be built and trusted before a single line of the Swift client existed.

A few lessons stood out:

Top view of two stacked bamboo boards forming the scale platform, with two screw heads on the upper board where the load-cell beam is mounted underneath
The platform from above — the two screws fix the cantilevered beam to the upper board; the boards must touch nowhere else.
  • Mechanics matter more than code. Maybe 80% of the noise and error is in how the beam is mounted. It has to be cantilevered — one end fixed, the other carrying the platform through spacers, with the middle free to flex. If the platform touches the base anywhere else, or a wire props it up, the load is shunted and the reading lies. Get the rig right first; then write firmware.
  • Trust the multimeter, not the wire colours. The “standard” colour code for a load cell’s four wires is right often enough to be dangerous. Ring out all six pairs before soldering — the two bridge diagonals read equal and highest.
  • A flipped sign isn’t a mistake. If weight goes negative under load, swap the two signal wires or flip the sign in firmware. It’s normal.
  • Zero drifts. Load cells creep under load and wander with temperature. The answer is taring before each weighing, not calibrating once and trusting it forever.

Where it’s going

The next step is the Swift ScaleService that turns the notify stream into live grams inside the app, followed by an enclosure and a battery so the scale runs without a USB host. But the core is proven: a cheap load cell and a $3 microcontroller now report grams I can trust, over a protocol I fully own — which is exactly what ScalePlate needed to stand on.