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

Gather all the parts
Assemble it in 7 steps
1. Prepare o ESP32-C3 SuperMini
Coloque o ESP32-C3 SuperMini em uma protoboard e mantenha a fonte externa desligada durante toda a montagem.
- Reserve GPIO 18 e GPIO 19 para USB nativa.
- Não use GPIO 9, normalmente ligado ao BOOT.
- Nunca aplique 5 V ao pino 3V3 ou aos GPIOs.
2. 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.
- Use fios SPI curtos.
- O display deve receber 3,3 V, não 5 V.
3. 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.
- 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.
4. 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.
- 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.
5. Conecte o botão Trip
Ligue um terminal do botão ao GPIO 3 e o outro ao GND.
- Em botão tátil de quatro pernas, escolha terminais de lados opostos.
6. 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.
- O LM2596 é redutor: a tensão de entrada deve ser maior que 5 V.
- 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.
7. 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.
- Todos os GNDs permanecem comuns pelo VOUT- do LM2596.
- 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.
Review all connections
1. Connections between "tft" and "ESP32"
2. Connections between "hall" and "ESP32"
3. Connections between "ds18b20" and "ESP32"
4. Connections between "btn_trip" and "ESP32"
5. Connections between "lm2596_5v" and "ESP32"
Deploy the firmware
#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);
}
}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.



