Smart Espresso Scale
Table
| Function | ESP32-S3 GPIO | Net |
|---|---|---|
| I2C SDA | 8 | I2C_SDA |
| I2C SCL | 9 | I2C_SCL |
| NAU7802 DRDY | 7 | NAU_DRDY |
| SD CS | 10 | SD_CS |
| SD MOSI | 11 | SD_MOSI |
| SD SCK | 12 | SD_SCK |
| SD MISO | 13 | SD_MISO |
| SD power enable | 21 | SD_PWR_EN |
| Tare button | 4 | BTN_TARE |
| Start/stop button | 5 | BTN_START_STOP |
| Status LED | 6 | LED_STATUS_DRV |
| Native USB D- | 19 | USB_D_N |
| Native USB D+ | 20 | USB_D_P |
| BOOT button | 0 | ESP_BOOT |
Ini
[env:esp32-s3-devkitc-1] platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino monitor_speed = 115200 build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 lib_deps = sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@^1.0.5 adafruit/Adafruit SSD1306@^2.5.10 adafruit/Adafruit GFX Library@^1.11.9 h2zero/NimBLE-Arduino@^1.4.2
Cpp
#include <Arduino.h> #include <Wire.h> #include <SPI.h> #include <SD.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <SparkFun_Qwiic_Scale_NAU7802_Arduino_Library.h> #include <NimBLEDevice.h> #define I2C_SDA_PIN 8 #define I2C_SCL_PIN 9 #define NAU_DRDY_PIN 7 #define SD_CS_PIN 10 #define SD_MOSI_PIN 11 #define SD_SCK_PIN 12 #define SD_MISO_PIN 13 #define SD_PWR_EN_PIN 21 #define BTN_TARE_PIN 4 #define BTN_START_STOP_PIN 5 #define STATUS_LED_PIN 6 #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); NAU7802 scale; NimBLEServer *bleServer = nullptr; NimBLECharacteristic *weightCharacteristic = nullptr; static const char *SERVICE_UUID = "b7a8f7e0-41a3-42e5-8c6e-espresso0001"; static const char *WEIGHT_UUID = "b7a8f7e0-41a3-42e5-8c6e-espresso0002"; float calibrationFactor = 1.0f; long zeroOffset = 0; bool timerRunning = false; uint32_t shotStartMs = 0; uint32_t lastSampleMs = 0; uint32_t lastLogMs = 0; File logFile; bool pressed(uint8_t pin) { return digitalRead(pin) == LOW; } void initBLE() { NimBLEDevice::init("EspressoScale"); bleServer = NimBLEDevice::createServer(); NimBLEService *service = bleServer->createService(SERVICE_UUID); weightCharacteristic = service->createCharacteristic( WEIGHT_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); weightCharacteristic->setValue("0.00,0"); service->start(); NimBLEAdvertising *adv = NimBLEDevice::getAdvertising(); adv->addServiceUUID(SERVICE_UUID); adv->start(); } void initDisplay() { if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println("OLED not found at 0x3C"); return; } display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(1); display.setCursor(0, 0); display.println("Smart Espresso Scale"); display.display(); } void initScale() { if (!scale.begin()) { Serial.println("NAU7802 not detected"); return; } scale.setGain(NAU7802_GAIN_128); scale.setSampleRate(NAU7802_SPS_80); scale.calibrateAFE(); delay(100); zeroOffset = scale.getAverage(16); } void initSD() { digitalWrite(SD_PWR_EN_PIN, HIGH); delay(20); SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN); if (!SD.begin(SD_CS_PIN, SPI)) { Serial.println("microSD init failed; continuing without local log"); return; } logFile = SD.open("/shots.csv", FILE_APPEND); if (logFile && logFile.size() == 0) { logFile.println("ms,grams,timer_ms"); logFile.flush(); } } float readGrams() { if (!scale.available()) return NAN; long raw = scale.getReading(); return (raw - zeroOffset) / calibrationFactor; } void updateDisplay(float grams, uint32_t timerMs) { display.clearDisplay(); display.setTextSize(2); display.setCursor(0, 0); display.printf("%6.1fg", grams); display.setTextSize(2); display.setCursor(0, 28); display.printf("%5.1fs", timerMs / 1000.0f); display.setTextSize(1); display.setCursor(0, 56); display.print(NimBLEDevice::getAdvertising()->isAdvertising() ? "BLE adv" : "BLE on"); display.print(logFile ? " SD" : " noSD"); display.display(); } void logSample(float grams, uint32_t timerMs) { if (!logFile) return; logFile.printf("%lu,%.2f,%lu\n", millis(), grams, timerMs); if (millis() - lastLogMs > 1000) { logFile.flush(); lastLogMs = millis(); } } void handleButtons() { static bool lastTare = false, lastStart = false; bool tareNow = pressed(BTN_TARE_PIN); bool startNow = pressed(BTN_START_STOP_PIN); if (tareNow && !lastTare) zeroOffset = scale.getAverage(16); if (startNow && !lastStart) { timerRunning = !timerRunning; if (timerRunning) shotStartMs = millis(); } lastTare = tareNow; lastStart = startNow; } void setup() { pinMode(STATUS_LED_PIN, OUTPUT); pinMode(SD_PWR_EN_PIN, OUTPUT); pinMode(BTN_TARE_PIN, INPUT_PULLUP); pinMode(BTN_START_STOP_PIN, INPUT_PULLUP); pinMode(NAU_DRDY_PIN, INPUT); digitalWrite(STATUS_LED_PIN, LOW); digitalWrite(SD_PWR_EN_PIN, LOW); Serial.begin(115200); delay(200); Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN, 400000); initDisplay(); initScale(); initSD(); initBLE(); } void loop() { handleButtons(); uint32_t now = millis(); if (now - lastSampleMs >= 50) { lastSampleMs = now; float grams = readGrams(); if (isnan(grams)) return; uint32_t timerMs = timerRunning ? now - shotStartMs : 0; updateDisplay(grams, timerMs); logSample(grams, timerMs); char payload[32]; snprintf(payload, sizeof(payload), "%.2f,%lu", grams, timerMs); weightCharacteristic->setValue(payload); weightCharacteristic->notify(); digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN)); } }
pio runpio run -t uploadpio device monitorcalibrationFactor from a known mass: tare the scale, place a calibration weight, record raw delta, then calibrationFactor = raw_delta / grams.Platform & Toolchain
Pin Mapping
platformio.ini
Complete Starter Source
Build & Flash
Calibration TODO