Community project
Bike Speed And Temperature Monitor
This project transforms an ESP32 into a real-time bike computer that displays current speed and ambient temperature on a bright 2-inch TFT display. A Hall effect sensor mounted on the wheel fork detects each rotation, while a DS18B20 temperature sensor provides live readings. A momentary button allows riders to reset trip data on the fly.
The guide includes a complete wiring diagram showing how to connect the ST7789 display, Hall sensor, temperature probe, and push button to the ESP32-C3 SuperMini, along with a parts list and step-by-step assembly instructions. The included firmware handles speed calculation from wheel circumference and magnet count, renders an analog-style gauge with needle animation, and manages the power supply via a 24V-to-5V buck converter for reliable operation in the field.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| ST7789 TFT Display 2.0 inch | 1 | 2.0-inch IPS TFT color display breakout driven by the ST7789 controller over 4-wire SPI. Native resolution is 320x240. Adafruit's breakout includes a 3.3V regulator, auto-reset circuit, 3V/5V level shifting, and a microSD holder sharing the SPI bus. Display drawing uses SCK, MOSI, CS, DC, and optional RST; MISO and SDCS are only needed for the onboard microSD card. |
| DS18B20 | 1 | Digital temperature sensor using OneWire protocol |
| Push Button (Momentary) | 1 | Momentary tactile push button. One side connects to GPIO with internal pull-up, other side to GND. Active-low: LOW when pressed, HIGH when released. |
| Módulo sensor Hall digital 3,3 V3,3 V | 1 | Módulo Hall digital alimentado em 3,3 V, conforme confirmado pelo usuário. Saída ativa em nível baixo e com pull-up no próprio módulo. |
| 24v Buck Converter5.0 V output | 1 | LM2596-based adjustable step-down buck converter module. Commonly used to regulate a higher battery rail, such as a 2S 18650 pack, down to 5V for Arduino logic. It is a regulator, not a charger or battery protection board. |
Assembly
7 stepsPrepare o ESP32-C3 SuperMini
Coloque o ESP32-C3 SuperMini em uma protoboard e mantenha a fonte externa desligada durante toda a montagem.
- Tip: Reserve GPIO 18 e GPIO 19 para USB nativa.
- Tip: Não use GPIO 9, normalmente ligado ao BOOT.
- ⚠ Nunca aplique 5 V ao pino 3V3 ou aos GPIOs.
Conecte o display ST7789
Ligue VCC ao 3V3, GND ao GND, SCK ao GPIO 6, MOSI ao GPIO 7, CS ao GPIO 10, DC ao GPIO 4 e RST ao GPIO 5.
- Tip: Use fios SPI curtos.
- ⚠ O display deve receber 3,3 V, não 5 V.
Conecte o DS18B20 sem resistor externo
Ligue VCC do DS18B20 ao 3V3, GND ao GND e DATA ao GPIO 1. Não instale resistor entre DATA e 3V3.
- Tip: Para o sensor avulso com a face plana voltada para você, os pinos são GND, DATA e VCC.
- ⚠ Esta ligação só funciona de modo confiável se seu módulo DS18B20 já tiver pull-up integrado na linha DATA.
Conecte o módulo Hall de 3,3 V
Ligue VCC ao 3V3, GND ao GND e OUT ao GPIO 0. O firmware não usa pull-up interno nesse pino.
- Tip: Mantenha o ímã a cerca de 5 mm ou menos do sensor.
- ⚠ O módulo Hall precisa ter pull-up próprio na saída OUT.
Conecte o botão Trip
Ligue um terminal do botão ao GPIO 3 e o outro ao GND.
- Tip: Em botão tátil de quatro pernas, escolha terminais de lados opostos.
Ligue o LM2596 à fonte de entrada
Com tudo desligado, conecte a fonte DC que alimentará o conversor aos terminais VIN+ e VIN- do LM2596, respeitando a polaridade.
- Tip: O LM2596 é redutor: a tensão de entrada deve ser maior que 5 V.
- Tip: Use uma fonte cuja corrente disponível seja de pelo menos 1 A.
- ⚠ Não inverta VIN+ e VIN-. Não conecte a entrada do LM2596 diretamente à rede elétrica.
Ajuste e conecte a saída de 5 V
Antes de ligar ao ESP32, energize o LM2596 e ajuste o trimpot medindo VOUT+ e VOUT- com multímetro até obter exatamente 5,0 V. Desligue a fonte; então ligue VOUT+ ao pad 5V/VBUS do ESP32-C3 SuperMini e VOUT- ao GND. Depois religue a fonte.
- Tip: Todos os GNDs permanecem comuns pelo VOUT- do LM2596.
- Tip: O ESP32 fornece 3V3 regulados ao TFT, Hall e DS18B20.
- ⚠ Não conecte uma saída acima de 5,0 V ao pad 5V/VBUS.
- ⚠ Evite conectar simultaneamente USB-C e o 5 V externo, salvo se houver proteção contra retorno de corrente.
Firmware
ESP32#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <math.h>
// ── Pin definitions ───────────────────────────────────────────────────────────
#define HALL_PIN 0
#define BTN_TRIP 3
#define TFT_CS 10
#define TFT_DC 4
#define TFT_RST 5
#define TFT_MOSI 7
#define TFT_SCK 6
#define TEMP_PIN 1 // DS18B20 alimentado pelo trilho 3V3
// ── Speedometer config ────────────────────────────────────────────────────────
#define WHEEL_CIRCUMFERENCE_M 2.05f
#define MAGNETS_PER_REV 1
#define SPEED_TIMEOUT_MS 3000
#define MAX_SPEED_GAUGE 60.0f // km/h at full arc
// ── Colour palette (RGB565) ───────────────────────────────────────────────────
#define C_BG 0x0841 // near-black blue-grey
#define C_PANEL 0x1082 // slightly lighter panel
#define C_CYAN 0x07FF
#define C_WHITE 0xFFFF
#define C_GREY 0x8410
#define C_DKGREY 0x2104
#define C_GREEN 0x07E0
#define C_ORANGE 0xFD20
#define C_RED 0xF800
#define C_BLUE 0x001F
#define C_YELLOW 0xFFE0
#define C_ACCENT 0x055F // teal accent
#define C_NEEDLE 0xF81F // magenta needle
// ── Gauge geometry ────────────────────────────────────────────────────────────
// Display is 320×240 (landscape). Gauge centre sits at (160, 138).
#define GCX 160
#define GCY 138
#define GR 100 // outer arc radius
#define GR_IN 82 // inner arc radius (thick arc)
#define GR_TK (GR - GR_IN) // arc thickness ≈ 18 px
// Arc spans from 210° to 330° (clockwise, 0° = 3 o'clock)
#define ARC_START_DEG 210.0f
#define ARC_END_DEG 330.0f // total 240°
// ── Display ───────────────────────────────────────────────────────────────────
// Forward declarations
float speedToAngle(float spd);
void drawArc(int cx, int cy, int r_out, int r_in, float a_start_deg, float a_end_deg, uint16_t colour, float step_deg);
uint16_t blendColor(uint16_t c1, uint16_t c2, float t);
uint16_t speedColor(float spd);
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCK, TFT_RST);
// ── DS18B20 ───────────────────────────────────────────────────────────────────
OneWire oneWire(TEMP_PIN);
DallasTemperature tempSensor(&oneWire);
float currentTempC = -127.0f;
// ── Speed state ───────────────────────────────────────────────────────────────
volatile unsigned long lastPulseTime = 0;
volatile unsigned long pulsePeriodMs = 0;
volatile bool newPulse = false;
volatile unsigned long tripPulses = 0;
float speedKmh = 0.0f;
float smoothSpeedKmh = 0.0f; // interpolated for animation
float maxSpeedKmh = 0.0f;
float totalDistKm = 0.0f;
// ── Button ────────────────────────────────────────────────────────────────────
unsigned long btnPressTime = 0;
bool btnWasHigh = true;
// ── Forward declarations ──────────────────────────────────────────────────────
void IRAM_ATTR hallISR();
void drawStaticBackground();
void drawGaugeArc(float spd, float maxSpd);
void drawSpeedDigits(float spd);
void drawStats(float maxSpd, float distKm);
void drawTempBar(float tempC);
void resetTrip();
void showResetFlash();
// ═════════════════════════════════════════════════════════════════════════════
// Helpers
// ═════════════════════════════════════════════════════════════════════════════
// Map a speed value to an angle in radians (0° = 3 o'clock, CW)
float speedToAngle(float spd) {
float frac = constrain(spd / MAX_SPEED_GAUGE, 0.0f, 1.0f);
float deg = ARC_START_DEG + frac * (ARC_END_DEG - ARC_START_DEG);
// Normalise to [0, 360), then convert to radian with display's CW convention
// Adafruit angles: 0=right, 90=down (y-down screen)
return deg * (float)M_PI / 180.0f;
}
// Draw a thick arc (approximated with filled triangles between successive steps)
// colour: RGB565. step_deg: angular resolution in degrees.
void drawArc(int cx, int cy, int r_out, int r_in,
float a_start_deg, float a_end_deg,
uint16_t colour, float step_deg = 2.0f) {
float a = a_start_deg * M_PI / 180.0f;
float a2 = a_end_deg * M_PI / 180.0f;
float step = step_deg * M_PI / 180.0f;
float cos_a = cosf(a), sin_a = sinf(a);
int x0o = cx + (int)(r_out * cos_a);
int y0o = cy + (int)(r_out * sin_a);
int x0i = cx + (int)(r_in * cos_a);
int y0i = cy + (int)(r_in * sin_a);
for (float ang = a + step; ang <= a2 + 0.001f; ang += step) {
float ca = cosf(ang), sa = sinf(ang);
int x1o = cx + (int)(r_out * ca);
int y1o = cy + (int)(r_out * sa);
int x1i = cx + (int)(r_in * ca);
int y1i = cy + (int)(r_in * sa);
tft.fillTriangle(x0o, y0o, x0i, y0i, x1i, y1i, colour);
tft.fillTriangle(x0o, y0o, x1o, y1o, x1i, y1i, colour);
x0o = x1o; y0o = y1o;
x0i = x1i; y0i = y1i;
}
}
// Blend two RGB565 colours — t in [0,1]
uint16_t blendColor(uint16_t c1, uint16_t c2, float t) {
uint8_t r1 = (c1 >> 11) & 0x1F, g1 = (c1 >> 5) & 0x3F, b1 = c1 & 0x1F;
uint8_t r2 = (c2 >> 11) & 0x1F, g2 = (c2 >> 5) & 0x3F, b2 = c2 & 0x1F;
uint8_t r = r1 + (uint8_t)((r2 - r1) * t);
uint8_t g = g1 + (uint8_t)((g2 - g1) * t);
uint8_t b = b1 + (uint8_t)((b2 - b1) * t);
return (r << 11) | (g << 5) | b;
}
// Speed → arc colour: green→yellow→orange→red
uint16_t speedColor(float spd) {
float frac = constrain(spd / MAX_SPEED_GAUGE, 0.0f, 1.0f);
if (frac < 0.5f) return blendColor(C_GREEN, C_YELLOW, frac * 2.0f);
else return blendColor(C_YELLOW, C_RED, (frac - 0.5f) * 2.0f);
}
// ═════════════════════════════════════════════════════════════════════════════
// Static background — drawn once (and after reset)
// ═════════════════════════════════════════════════════════════════════════════
void drawStaticBackground() {
tft.fillScreen(C_BG);
// ── Top bar ───────────────────────────────────────────────────────────────
tft.fillRect(0, 0, 320, 26, C_PANEL);
// Title
tft.setTextColor(C_CYAN);
tft.setTextSize(2);
tft.setCursor(8, 5);
tft.print("VELOCI");
// Small "METRO" superscript-style
tft.setTextSize(1);
tft.setTextColor(C_GREY);
tft.setCursor(74, 8);
tft.print("METRO");
// Thin accent line under header
tft.drawFastHLine(0, 26, 320, C_ACCENT);
// ── Bottom panel ──────────────────────────────────────────────────────────
// Panel background
tft.fillRect(0, 196, 320, 44, C_PANEL);
tft.drawFastHLine(0, 196, 320, C_ACCENT);
// Dividers
tft.drawFastVLine(106, 196, 44, C_ACCENT);
tft.drawFastVLine(213, 196, 44, C_ACCENT);
// Labels
tft.setTextSize(1);
tft.setTextColor(C_GREY);
tft.setCursor(18, 200);
tft.print("MAX km/h");
tft.setCursor(118, 200);
tft.print("DIST km");
tft.setCursor(222, 200);
tft.print("TEMP " "\xF7" "C"); // ÷ symbol as degree approximation
// ── Gauge track (background arc) ─────────────────────────────────────────
drawArc(GCX, GCY, GR, GR_IN, ARC_START_DEG, ARC_END_DEG, C_DKGREY, 2.0f);
// ── Tick marks ───────────────────────────────────────────────────────────
// 7 major ticks: 0,10,20,30,40,50,60 km/h
for (int i = 0; i <= 6; i++) {
float spd = i * 10.0f;
float frac = spd / MAX_SPEED_GAUGE;
float deg = ARC_START_DEG + frac * (ARC_END_DEG - ARC_START_DEG);
float rad = deg * M_PI / 180.0f;
float ca = cosf(rad), sa = sinf(rad);
int x0 = GCX + (int)((GR + 4) * ca);
int y0 = GCY + (int)((GR + 4) * sa);
int x1 = GCX + (int)((GR + 12) * ca);
int y1 = GCY + (int)((GR + 12) * sa);
tft.drawLine(x0, y0, x1, y1, C_GREY);
// Tick label (skip 0 to avoid clutter near 210°)
if (i > 0) {
int lx = GCX + (int)((GR + 20) * ca) - 6;
int ly = GCY + (int)((GR + 20) * sa) - 4;
tft.setTextSize(1);
tft.setTextColor(C_GREY);
tft.setCursor(lx, ly);
tft.print(i * 10);
}
}
// Minor ticks (every 5 km/h)
for (int i = 0; i <= 12; i++) {
if (i % 2 == 0) continue; // skip major tick positions
float spd = i * 5.0f;
float frac = spd / MAX_SPEED_GAUGE;
float deg = ARC_START_DEG + frac * (ARC_END_DEG - ARC_START_DEG);
float rad = deg * M_PI / 180.0f;
float ca = cosf(rad), sa = sinf(rad);
int x0 = GCX + (int)((GR + 4) * ca);
int y0 = GCY + (int)((GR + 4) * sa);
int x1 = GCX + (int)((GR + 8) * ca);
int y1 = GCY + (int)((GR + 8) * sa);
tft.drawLine(x0, y0, x1, y1, C_DKGREY);
}
// ── "km/h" label inside gauge ─────────────────────────────────────────────
tft.setTextSize(1);
tft.setTextColor(C_GREY);
tft.setCursor(GCX - 12, GCY + 54);
tft.print("km/h");
}
// ═════════════════════════════════════════════════════════════════════════════
// Gauge arc — redrawn on every 200 ms tick
// ═════════════════════════════════════════════════════════════════════════════
void drawGaugeArc(float spd, float maxSpd) {
// 1. Erase the gauge area (preserve background tracks drawn as static)
// Re-draw the grey track first, then overlay the coloured arc.
drawArc(GCX, GCY, GR, GR_IN, ARC_START_DEG, ARC_END_DEG, C_DKGREY, 2.0f);
// 2. Coloured speed arc
if (spd > 0.5f) {
float fracSpd = constrain(spd / MAX_SPEED_GAUGE, 0.0f, 1.0f);
float endDeg = ARC_START_DEG + fracSpd * (ARC_END_DEG - ARC_START_DEG);
uint16_t col = speedColor(spd);
drawArc(GCX, GCY, GR, GR_IN, ARC_START_DEG, endDeg, col, 2.0f);
}
// 3. Max-speed marker (small bright dot on the outer rim)
if (maxSpd > 0.5f) {
float fracMax = constrain(maxSpd / MAX_SPEED_GAUGE, 0.0f, 1.0f);
float degMax = ARC_START_DEG + fracMax * (ARC_END_DEG - ARC_START_DEG);
float rad = degMax * M_PI / 180.0f;
int mx = GCX + (int)(GR * cosf(rad));
int my = GCY + (int)(GR * sinf(rad));
tft.fillCircle(mx, my, 4, C_ORANGE);
tft.drawCircle(mx, my, 5, C_WHITE);
}
// The centre remains clear for the speed digits.
}
// ═════════════════════════════════════════════════════════════════════════════
// Speed digits
// ═════════════════════════════════════════════════════════════════════════════
void drawSpeedDigits(float spd) {
// Clear the digit area (inside the gauge)
tft.fillRect(GCX - 72, GCY - 36, 144, 52, C_BG);
char buf[7];
if (spd < 10.0f)
snprintf(buf, sizeof(buf), " %.1f", spd);
else if (spd < 100.0f)
snprintf(buf, sizeof(buf), "%.1f", spd);
else
snprintf(buf, sizeof(buf), "%.0f", spd);
tft.setTextSize(5);
// Colour matches the arc
uint16_t col = (spd < 1.0f) ? C_GREY : speedColor(spd);
tft.setTextColor(col);
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
tft.setCursor(GCX - w / 2, GCY - h / 2 - 4);
tft.print(buf);
}
// ═════════════════════════════════════════════════════════════════════════════
// Bottom stats panel
// ═════════════════════════════════════════════════════════════════════════════
void drawStats(float maxSpd, float distKm) {
char buf[10];
// MAX speed
tft.fillRect(1, 212, 104, 26, C_PANEL);
tft.setTextSize(2);
tft.setTextColor(C_ORANGE);
snprintf(buf, sizeof(buf), "%5.1f", maxSpd);
tft.setCursor(8, 214);
tft.print(buf);
// Distance
tft.fillRect(108, 212, 104, 26, C_PANEL);
tft.setTextColor(C_GREEN);
snprintf(buf, sizeof(buf), "%6.2f", distKm);
tft.setCursor(112, 214);
tft.print(buf);
}
// ═════════════════════════════════════════════════════════════════════════════
// Temperature — coloured value + small bar
// ═════════════════════════════════════════════════════════════════════════════
void drawTempBar(float tempC) {
tft.fillRect(215, 212, 104, 26, C_PANEL);
if (tempC <= -126.0f) {
tft.setTextSize(2);
tft.setTextColor(C_GREY);
tft.setCursor(222, 214);
tft.print(" ---");
return;
}
// Colour ramp: blue < 10 → cyan < 20 → green < 30 → orange < 40 → red
uint16_t col;
if (tempC < 10.0f) col = C_BLUE;
else if (tempC < 20.0f) col = C_CYAN;
else if (tempC < 30.0f) col = C_GREEN;
else if (tempC < 40.0f) col = C_ORANGE;
else col = C_RED;
// Numeric value
char tb[8];
snprintf(tb, sizeof(tb), "%5.1f", tempC);
tft.setTextSize(2);
tft.setTextColor(col);
tft.setCursor(218, 214);
tft.print(tb);
// Mini horizontal bar (bottom of cell)
float frac = constrain((tempC - (-10.0f)) / 60.0f, 0.0f, 1.0f);
int barW = (int)(96 * frac);
tft.drawFastHLine(216, 236, 96, C_DKGREY);
if (barW > 0)
tft.drawFastHLine(216, 236, barW, col);
}
// ═════════════════════════════════════════════════════════════════════════════
// Trip reset
// ═════════════════════════════════════════════════════════════════════════════
void showResetFlash() {
// Overlay banner over the gauge area
tft.fillRoundRect(50, 88, 220, 56, 10, C_GREEN);
tft.drawRoundRect(50, 88, 220, 56, 10, C_WHITE);
tft.setTextSize(2);
tft.setTextColor(C_BG);
tft.setCursor(74, 102);
tft.print("TRIP RESET!");
tft.setTextSize(1);
tft.setTextColor(C_BG);
tft.setCursor(88, 124);
tft.print("segure p/ confirmar");
delay(700);
}
void resetTrip() {
noInterrupts();
tripPulses = 0;
interrupts();
maxSpeedKmh = 0.0f;
totalDistKm = 0.0f;
smoothSpeedKmh = 0.0f;
Serial.println("[TRIP] Resetado!");
showResetFlash();
drawStaticBackground();
drawGaugeArc(0.0f, 0.0f);
drawSpeedDigits(0.0f);
drawStats(0.0f, 0.0f);
drawTempBar(currentTempC);
}
// ═════════════════════════════════════════════════════════════════════════════
// ISR
// ═════════════════════════════════════════════════════════════════════════════
void IRAM_ATTR hallISR() {
unsigned long now = millis();
unsigned long period = now - lastPulseTime;
if (period < 20) return; // debounce
pulsePeriodMs = period;
lastPulseTime = now;
newPulse = true;
tripPulses++;
}
// ═════════════════════════════════════════════════════════════════════════════
// Setup
// ═════════════════════════════════════════════════════════════════════════════
void setup() {
Serial.begin(115200);
// Display init — 320×240 landscape
tft.init(240, 320);
tft.setRotation(1);
drawStaticBackground();
drawGaugeArc(0.0f, 0.0f);
drawSpeedDigits(0.0f);
drawStats(0.0f, 0.0f);
drawTempBar(-127.0f);
// DS18B20 — non-blocking mode
tempSensor.begin();
tempSensor.setResolution(11);
tempSensor.setWaitForConversion(false);
tempSensor.requestTemperatures();
// Hall sensor module powered at 3.3 V — active-low, no internal pull-up.
// Its OUT signal is already at ESP32-safe 3.3 V.
pinMode(HALL_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(HALL_PIN), hallISR, FALLING);
// Trip reset button — pull-up
pinMode(BTN_TRIP, INPUT_PULLUP);
lastPulseTime = millis();
Serial.println("[VELOCI] Iniciado!");
}
// ═════════════════════════════════════════════════════════════════════════════
// Loop
// ═════════════════════════════════════════════════════════════════════════════
void loop() {
static float lastGaugeSpd = -1.0f;
static float lastGaugeMax = -1.0f;
static float lastDigitSpd = -1.0f;
static float lastMaxSpd = -1.0f;
static float lastDist = -1.0f;
static float lastTemp = -999.0f;
static unsigned long lastUpdate = 0;
static unsigned long lastTempReq = 0;
unsigned long now = millis();
// ── Button debounce & trip reset ─────────────────────────────────────────
bool btnNow = digitalRead(BTN_TRIP);
if (btnWasHigh && !btnNow) btnPressTime = now;
if (!btnWasHigh && btnNow && (now - btnPressTime) >= 50) resetTrip();
btnWasHigh = btnNow;
// ── Speed timeout ─────────────────────────────────────────────────────────
if ((now - lastPulseTime) > SPEED_TIMEOUT_MS) speedKmh = 0.0f;
// ── Compute speed from new pulse ──────────────────────────────────────────
if (newPulse) {
noInterrupts();
unsigned long period = pulsePeriodMs;
newPulse = false;
interrupts();
if (period > 0) {
float periodSec = period / 1000.0f;
float speedMs = (WHEEL_CIRCUMFERENCE_M / MAGNETS_PER_REV) / periodSec;
speedKmh = speedMs * 3.6f;
}
}
// ── Distance ─────────────────────────────────────────────────────────────
noInterrupts();
unsigned long pulses = tripPulses;
interrupts();
totalDistKm = (pulses * WHEEL_CIRCUMFERENCE_M) /
(1000.0f * MAGNETS_PER_REV);
// ── Max speed ─────────────────────────────────────────────────────────────
if (speedKmh > maxSpeedKmh) maxSpeedKmh = speedKmh;
// ── Smooth speed animation (exponential filter) ───────────────────────────
// α = 0.35 → snappy but not jittery
smoothSpeedKmh = smoothSpeedKmh * 0.65f + speedKmh * 0.35f;
if (smoothSpeedKmh < 0.3f) smoothSpeedKmh = 0.0f; // snap to zero at low vals
// ── Display update at 200 ms ─────────────────────────────────────────────
if (now - lastUpdate >= 200) {
lastUpdate = now;
// Gauge arc — redraw if speed or max changed noticeably
bool gaugeChanged =
fabsf(smoothSpeedKmh - lastGaugeSpd) >= 0.3f ||
fabsf(maxSpeedKmh - lastGaugeMax) >= 0.2f;
if (gaugeChanged) {
drawGaugeArc(smoothSpeedKmh, maxSpeedKmh);
lastGaugeSpd = smoothSpeedKmh;
lastGaugeMax = maxSpeedKmh;
}
// Speed digits
if (fabsf(smoothSpeedKmh - lastDigitSpd) >= 0.1f) {
drawSpeedDigits(smoothSpeedKmh);
lastDigitSpd = smoothSpeedKmh;
}
// Stats bar
bool statsChanged =
fabsf(maxSpeedKmh - lastMaxSpd) >= 0.1f ||
fabsf(totalDistKm - lastDist) >= 0.01f;
if (statsChanged) {
drawStats(maxSpeedKmh, totalDistKm);
lastMaxSpd = maxSpeedKmh;
lastDist = totalDistKm;
}
// Temperature every 2 s
if (now - lastTempReq >= 2000) {
float t = tempSensor.getTempCByIndex(0);
if (t > -126.0f) currentTempC = t;
tempSensor.requestTemperatures();
lastTempReq = now;
if (fabsf(currentTempC - lastTemp) >= 0.2f) {
drawTempBar(currentTempC);
lastTemp = currentTempC;
}
}
// Serial log
Serial.printf("[VEL] %.1f km/h (smooth %.1f) | MAX %.1f | DIST %.2f km | TEMP %.1f C\n",
speedKmh, smoothSpeedKmh, maxSpeedKmh, totalDistKm, currentTempC);
}
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
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.