Community project

Giám Sát Điện Xe Máy

ESP32
Photo of Giám Sát Điện Xe Máy
Generated with AI

Vũ Duy Nguyễn

Published October 13, 2026

This guide builds a motorcycle electrical monitoring system using an ESP32 microcontroller to track battery voltage, current draw, power consumption, and GPS location in real time. The system reads a 12 V motorcycle battery through voltage dividers and measures current with an ACS712 sensor, then streams all data to a web interface accessible from a smartphone.

The project includes a complete wiring diagram, parts list with resistor values for safe analog measurement, step-by-step assembly instructions, and ready-to-upload firmware. Builders will learn how to interface GPS modules with the ESP32, create voltage dividers for safe ADC readings, and set up a WiFi access point for remote monitoring of energy consumption and trip distance.

Wiring diagram

Wiring diagram for Giám Sát Điện Xe Máy

Gather all the parts

QtyComponent
1

ACS712 Current Sensor

30 A

Hall-effect AC/DC current sensor breakout. It outputs an analog voltage centered around VCC/2 at zero current; sensitivity depends on the 5 A, 20 A, or 30 A variant. Most modules run from 5 V and can output up to 5 V, so connect VIOUT to a 3.3 V MCU ADC through a divider or clamp.

1

NEO-6M GPS Module

u-blox NEO-6M based GPS receiver module. Outputs NMEA sentences (GGA, RMC, etc.) over UART at 9600 baud by default. Provides latitude, longitude, altitude, speed, and time. The NEO-6M module itself is a 3.3V-class device; many GY-NEO6MV2 breakout boards accept 5V on their VCC header through an onboard regulator, but UART I/O remains 3.3V-domain and must not be driven above 3.6V. Features an on-board patch antenna footprint and an SMA/IPEX connector for external active antenna (preferred for faster lock acquisition). Supply current ~45 mA in acquisition, ~11 mA in tracking.

1

12 V to 5 V DC-DC buck converter module

A ready-made converter that safely turns the motorcycle's 12 V supply into 5 V for the ESP32 and current sensor.

1

20 A inline blade fuse and holder

A holder with a 20 A automotive fuse that protects the monitored accessory branch from a short circuit.

1

10 kΩ resistor

10 kΩ

A resistor that helps lower the ACS712's 5 V signal to a safe level for the ESP32 input.

1

22 kΩ resistor

22 kΩ

A resistor from the ACS712 signal-reading point to ground, completing the safe signal divider.

1

180 kΩ resistor

180 kΩ

A high-value resistor that reduces motorcycle battery voltage before it reaches the ESP32 voltage-reading point.

1

22 kΩ resistor

22 kΩ

A resistor from the battery-voltage reading point to ground, completing the battery voltage divider.

1

3.3 V Zener diode

3.3 V

A protective diode that clamps an unexpected high voltage at the ESP32 battery-voltage reading point.

1

1 µF ceramic capacitor

1 µF

A small capacitor that smooths noise from the ACS712 current sensor.

1

12 V motorcycle battery

12 V

The motorcycle's existing 12 V battery that powers the monitor and is measured by the voltage divider.

Assemble it in 6 steps

1. Ngắt điện trước khi lắp

Tắt máy, rút chìa khóa và tháo cọc âm của bình 12 V trước. Đặt ESP32, mạch ACS712 và mạch GPS ở nơi khô, không chạm vào khung sườn kim loại.

  • Dùng hộp nhựa kín và cố định dây để rung xe không làm đứt mối nối.
  • Không chạm hai đầu bình bằng dụng cụ kim loại — bình có thể tạo tia lửa và làm nóng dây rất nhanh.

2. Lắp cầu chì và cảm biến dòng

Nối cực dương bình vào chân IN của cầu chì 20 A, rồi nối chân OUT của cầu chì vào IP+ trên ACS712. Nối IP- của ACS712 đến dây dương cấp cho nhánh thiết bị phụ tải cần đo; từ chính IP- này nối thêm dây đến VIN+ của mạch hạ áp. Chiều IP+ → IP- là chiều dòng dương để trang web ghi số đúng.

  • Chỉ đưa nhánh phụ tải qua ACS712; các thiết bị không đi qua IP+ và IP- sẽ không được cộng vào Ah/Wh.
  • Không nối dây mô-tơ đề qua ACS712 hoặc cầu chì 20 A — dòng đề có thể vượt 30 A, làm nóng cháy mạch hoặc dây.

3. Cấp nguồn 5 V cho ESP32

Nối VIN- của mạch hạ áp vào cực âm bình. Chỉnh mạch hạ áp ra đúng 5.0 V trước khi cắm ESP32. Sau đó nối VOUT+ vào chân VIN/5V của ESP32 và VOUT- vào GND. Nối VOUT+ đến VCC của ACS712, và nối GND của ACS712 vào GND chung.

  • Đo đầu ra mạch hạ áp bằng đồng hồ trước; 5 V là nguồn, GND là đường về chung.
  • Đừng cấp 12 V trực tiếp vào chân 3V3, VIN/5V hoặc bất kỳ chân GPIO nào của ESP32 — điều đó có thể làm hỏng bo mạch ngay lập tức.

4. Đấu mạch GPS

Nối VCC của NEO-6M vào 3V3 ESP32, GND vào GND ESP32, TX của GPS vào GPIO17 ESP32 và RX của GPS vào GPIO18 ESP32. Đặt mặt ăng-ten gốm của GPS hướng lên trời, tránh bị che bởi kim loại.

  • VCC → 3V3 (power); GND → GND (ground); TX → GPIO17 (dữ liệu GPS vào ESP32); RX → GPIO18 (dữ liệu cấu hình GPS).
  • Không cấp 5 V vào chân dữ liệu TX/RX của GPS — dữ liệu GPS là mức 3.3 V.

5. Làm hai mạch hạ điện áp cho chân đọc

Từ VIOUT của ACS712, nối qua điện trở 10 kΩ đến GPIO4; cùng điểm GPIO4 nối điện trở 22 kΩ xuống GND. Nối chân FILTER ACS712 với một đầu tụ 1 µF, đầu còn lại của tụ xuống GND. Từ chân OUT của cầu chì, nối qua điện trở 180 kΩ đến GPIO5; cùng điểm GPIO5 nối điện trở 22 kΩ xuống GND và nối đầu có vạch của diode Zener 3.3 V vào GPIO5, đầu không vạch xuống GND.

  • VIOUT → 10 kΩ → GPIO4 (signal); GPIO4 → 22 kΩ → GND (ground); cầu chì OUT → 180 kΩ → GPIO5 (signal); GPIO5 → 22 kΩ → GND (ground).
  • Đầu có vạch của diode Zener phải ở GPIO5; lắp ngược sẽ mất lớp bảo vệ chân đọc điện áp.

6. Kết nối điện thoại với trang web

Gắn lại cọc âm bình, cắm ESP32 qua USB lần đầu và dùng nút Deploy trong Schematik để nạp chương trình. Sau đó trên điện thoại kết nối Wi‑Fi tên XeMonitor, mật khẩu motobike123, rồi mở trình duyệt đến địa chỉ 192.168.4.1. Nhập dung lượng Ah ghi trên nhãn bình để trang web tính phần Ah đã dùng.

  • Chờ GPS 1–5 phút ở nơi thoáng trời cho lần bắt vệ tinh đầu tiên.
  • Không nhìn hoặc thao tác điện thoại khi đang lái xe; chỉ xem trang web khi xe đã dừng an toàn.

Review all connections

1. Connections between "fuse_20a" and "ESP32"

Functionfuse_20aESP32
powerOUT → ACS712 Current Sensor IP+EXT
powerOUT → 180 kΩ resistor End AEXT
powerIN → 12 V motorcycle battery Positive +EXT

2. Connections between "acs712_30a" and "ESP32"

Functionacs712_30aESP32
dataIP- → 12 V to 5 V DC-DC buck converter module VIN+EXT
dataIP- → Positive lead of the accessory load being monitored (not starter motor)EXT
powerVCC5V
groundGNDGND
analogVIOUT → 10 kΩ resistor End AEXT
analogFILTER → 1 µF ceramic capacitor Positive/End 1EXT

3. Connections between "buck_12v_5v" and "ESP32"

Functionbuck_12v_5vESP32
powerVOUT+VIN
powerVOUT+ → ACS712 Current Sensor VCCEXT
groundVOUT-GND
groundVIN- → 12 V motorcycle battery Negative -EXT

4. Connections between "gps_neo6m" and "ESP32"

Functiongps_neo6mESP32
powerVCC3V3
groundGNDGND
uartTXGPIO 17
uartRXGPIO 18

5. Connections between "r_acs_top_10k" and "ESP32"

Functionr_acs_top_10kESP32
analogEnd BGPIO 4

6. Connections between "r_acs_bottom_22k" and "ESP32"

Functionr_acs_bottom_22kESP32
groundEnd BGND
analogEnd A → 10 kΩ resistor End BEXT

7. Connections between "r_batt_top_180k" and "ESP32"

Functionr_batt_top_180kESP32
analogEnd BGPIO 5

8. Connections between "r_batt_bottom_22k" and "ESP32"

Functionr_batt_bottom_22kESP32
groundEnd BGND
analogEnd A → 180 kΩ resistor End BEXT

9. Connections between "zener_3v3" and "ESP32"

Functionzener_3v3ESP32
groundAnodeGND
analogCathode → 180 kΩ resistor End BEXT

10. Connections between "cap_filter_1uf" and "ESP32"

Functioncap_filter_1ufESP32
groundNegative/End 2GND

Deploy the firmware

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <TinyGPS++.h>

constexpr int ACS_ADC_PIN = 4;
constexpr int BATTERY_ADC_PIN = 5;
constexpr int GPS_RX_PIN = 17;
constexpr int GPS_TX_PIN = 18;
constexpr float ACS_DIVIDER_GAIN = 32.0f / 22.0f;
constexpr float BATTERY_DIVIDER_GAIN = 202.0f / 22.0f;
constexpr float ACS_ZERO_VOLTS = 2.50f;
constexpr float ACS_SENSITIVITY_V_PER_A = 0.066f;
constexpr float NOISE_CURRENT_A = 0.12f;

const char *AP_NAME = "XeMonitor";
const char *AP_PASSWORD = "motobike123";

WebServer server(80);
TinyGPSPlus gps;
HardwareSerial gpsSerial(1);

float batteryVoltage = 0.0f;
float currentAmps = 0.0f;
float powerWatts = 0.0f;
float usedAh = 0.0f;
float usedWh = 0.0f;
float distanceKm = 0.0f;
float capacityAh = 7.0f;
unsigned long lastEnergyMs = 0;
double lastLatitude = 0.0;
double lastLongitude = 0.0;
bool haveLastGpsPoint = false;

float readAdcVolts(int pin) {
  uint32_t totalMv = 0;
  for (int i = 0; i < 16; ++i) {
    totalMv += analogReadMilliVolts(pin);
  }
  return (totalMv / 16.0f) / 1000.0f;
}

const char *batteryState(float volts) {
  if (volts >= 13.6f) return "Đang sạc";
  if (volts >= 12.6f) return "Đầy / tốt";
  if (volts >= 12.2f) return "Bình thường";
  if (volts >= 11.9f) return "Yếu";
  return "Rất yếu";
}

void updateMeasurements() {
  while (gpsSerial.available()) {
    gps.encode(gpsSerial.read());
  }

  if (gps.location.isValid() && gps.location.isUpdated()) {
    double latitude = gps.location.lat();
    double longitude = gps.location.lng();
    if (haveLastGpsPoint) {
      double segmentMeters = TinyGPSPlus::distanceBetween(lastLatitude, lastLongitude, latitude, longitude);
      // Ignore impossible GPS jumps while the receiver is settling.
      if (segmentMeters < 250.0) distanceKm += segmentMeters / 1000.0f;
    }
    lastLatitude = latitude;
    lastLongitude = longitude;
    haveLastGpsPoint = true;
  }

  batteryVoltage = readAdcVolts(BATTERY_ADC_PIN) * BATTERY_DIVIDER_GAIN;
  float acsOutputVolts = readAdcVolts(ACS_ADC_PIN) * ACS_DIVIDER_GAIN;
  currentAmps = (acsOutputVolts - ACS_ZERO_VOLTS) / ACS_SENSITIVITY_V_PER_A;
  if (currentAmps < NOISE_CURRENT_A) currentAmps = 0.0f;
  powerWatts = batteryVoltage * currentAmps;

  unsigned long now = millis();
  if (lastEnergyMs != 0) {
    float hours = (now - lastEnergyMs) / 3600000.0f;
    usedAh += currentAmps * hours;
    usedWh += powerWatts * hours;
  }
  lastEnergyMs = now;
}

String dashboardPage() {
  return R"HTML(<!doctype html><html lang="vi"><head><meta name="viewport" content="width=device-width,initial-scale=1"><title>XE-MONITOR</title><style>*{box-sizing:border-box}body{margin:0;background:#edf2fb;color:#edf3ff;font-family:Arial,sans-serif}.phone{max-width:440px;min-height:100vh;margin:auto;padding:22px 16px;background:#07111d}.top{display:flex;justify-content:space-between;align-items:center;color:#aebcd0;font-weight:bold;letter-spacing:.5px}.online{font-size:.82rem;color:#9eacc0}.dot{display:inline-block;width:9px;height:9px;background:#2dd68c;border-radius:50%;margin-right:6px}.speedbox{margin-top:14px;background:#eef3ff;color:#12213a;border-radius:24px;padding:18px 20px}.sat{font-size:.88rem;font-weight:bold;color:#53637e}.gdot{color:#22c780;font-size:1.2rem}.dial{height:190px;position:relative;overflow:hidden;text-align:center;background:radial-gradient(ellipse at 50% 105%,transparent 0 53%,#d8e4fb 54% 56%,transparent 57%),conic-gradient(from 225deg at 50% 105%,#d6e3fa 0 220deg,transparent 221deg);border-radius:170px 170px 0 0;margin-top:4px}.km{position:absolute;right:17px;top:68px;font-size:1.1rem;font-weight:bold;font-style:italic}.speed{position:absolute;right:28px;top:80px;font-size:5.4rem;font-weight:bold;font-style:italic;line-height:1}.gpsbar{display:flex;gap:10px;align-items:center;background:#1a2b51;color:white;border-radius:18px;padding:9px 12px;font-size:.78rem;font-weight:bold}.bar{height:8px;flex:1;background:#51648d;border-radius:8px;overflow:hidden}.fill{height:100%;width:0%;background:#e85363;transition:width .3s}.grid{display:grid;grid-template-columns:1fr 1fr;gap:11px;margin-top:13px}.card{background:#111e2b;border-radius:15px;padding:15px;min-height:103px}.wide{grid-column:1/3;min-height:86px}.label{color:#a7b4c6;font-size:.86rem}.value{font-size:1.9rem;font-weight:bold;margin-top:8px}.unit{font-size:.85rem;color:#b5c1cf}.status{font-size:.78rem;margin-top:7px}.blue{color:#79d8ff}.buttons{display:flex;gap:10px;margin-top:14px}button{width:100%;border:1px solid #3c506d;border-radius:11px;background:#142238;color:#f2f6ff;padding:12px;font-size:.91rem}input{width:100%;margin-top:14px;padding:10px;border-radius:10px;border:1px solid #3c506d;background:#142238;color:white;font-size:1rem}.note{color:#9baabe;font-size:.72rem;line-height:1.35;margin:14px 3px}</style></head><body><main class="phone"><div class="top"><span>XE-MONITOR</span><span class="online"><i class="dot"></i>192.168.4.1</span></div><section class="speedbox"><div class="sat"><span class="gdot">●</span> <span id="sat">Đang tìm vệ tinh</span></div><div class="dial"><span class="km">KM/H</span><strong class="speed" id="speed">--</strong></div><div class="gpsbar"><span>GPS</span><div class="bar"><div class="fill" id="gpsFill"></div></div></div></section><section class="grid"><div class="card"><div class="label">Điện áp</div><div class="value blue"><span id="v">--</span><span class="unit"> V</span></div><div class="status" id="state">Đang chờ đo</div></div><div class="card"><div class="label">Dòng điện</div><div class="value blue"><span id="a">--</span><span class="unit"> A</span></div><div class="status" id="load">Đang chờ đo</div></div><div class="card"><div class="label">Đã tiêu thụ</div><div class="value blue"><span id="ah">--</span><span class="unit"> Ah</span></div><div class="status" id="wh">-- Wh</div></div><div class="card"><div class="label">Công suất</div><div class="value"><span id="w">--</span><span class="unit"> W</span></div><div class="status">Tức thời</div></div><div class="card wide"><div class="label">Quãng đường</div><div class="value blue"><span id="dist">--</span><span class="unit"> km</span></div></div></section><input id="cap" type="number" min="1" step="0.1" value="7" placeholder="Dung lượng bình (Ah)"><div class="buttons"><button onclick="resetAll()">Reset Ah &amp; km</button><button onclick="zeroCurrent()">Chỉnh về 0 (dòng)</button></div><p class="note">Ah, Wh và km được tính từ lúc bật hoặc nhấn Reset. Chỉ các tải nối qua IP+ → IP- của ACS712 mới được tính.</p></main><script>function f(n,d=2){return Number(n).toFixed(d)}async function tick(){try{let x=await (await fetch('/data')).json();v.textContent=f(x.v,1);state.textContent=x.state;a.textContent=f(x.a,1);load.textContent=x.a>0.05?'Đang xả':'Không tải';ah.textContent=f(x.ah,3);wh.textContent=f(x.wh,1)+' Wh';w.textContent=f(x.w,0);dist.textContent=f(x.km,2);speed.textContent=x.gpsValid?f(x.kmh,0):'--';sat.textContent=x.gpsValid?'Đã bắt được GPS':'Đang tìm vệ tinh';gpsFill.style.width=x.gpsValid?'100%':'8%'}catch(e){}}async function resetAll(){await fetch('/reset');tick()}async function zeroCurrent(){alert('Đưa tải về 0 A trước khi hiệu chuẩn. Tính năng này cần thêm nút hiệu chuẩn phần cứng nếu muốn lưu lâu dài.')}cap.onchange=()=>fetch('/capacity?ah='+encodeURIComponent(cap.value));setInterval(tick,1000);tick();</script></body></html>)HTML";
}

void handleData() {
  bool valid = gps.location.isValid() && gps.location.age() < 5000;
  float pct = capacityAh > 0.0f ? (usedAh / capacityAh) * 100.0f : 0.0f;
  String json = "{\"v\":" + String(batteryVoltage, 2) + ",\"a\":" + String(currentAmps, 2) + ",\"w\":" + String(powerWatts, 1) + ",\"ah\":" + String(usedAh, 4) + ",\"wh\":" + String(usedWh, 2) + ",\"capacity\":" + String(capacityAh, 1) + ",\"usedPct\":" + String(pct, 1) + ",\"km\":" + String(distanceKm, 2) + ",\"state\":\"" + batteryState(batteryVoltage) + "\",\"gpsValid\":" + String(valid ? "true" : "false") + ",\"kmh\":" + String(valid ? gps.speed.kmph() : 0.0f, 1) + ",\"lat\":" + String(valid ? gps.location.lat() : 0.0, 6) + ",\"lng\":" + String(valid ? gps.location.lng() : 0.0, 6) + "}";
  server.send(200, "application/json", json);
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(12); 
  analogSetPinAttenuation(ACS_ADC_PIN, ADC_11db);
  analogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);
  gpsSerial.begin(9600, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
  WiFi.mode(WIFI_AP);
  WiFi.softAP(AP_NAME, AP_PASSWORD);
  server.on("/", []() { server.send(200, "text/html; charset=utf-8", dashboardPage()); });
  server.on("/data", handleData);
  server.on("/capacity", []() { if (server.hasArg("ah")) capacityAh = constrain(server.arg("ah").toFloat(), 1.0f, 200.0f); server.send(200, "text/plain", "OK"); });
  server.on("/reset", []() { usedAh = 0.0f; usedWh = 0.0f; distanceKm = 0.0f; haveLastGpsPoint = false; lastEnergyMs = millis(); server.send(200, "text/plain", "OK"); });
  server.begin();
  lastEnergyMs = millis();
}

void loop() {
  updateMeasurements();
  server.handleClient();
  delay(100);
}

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