Community project
Giám Sát Điện Xe Máy
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

Gather all the parts
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"
2. Connections between "acs712_30a" and "ESP32"
3. Connections between "buck_12v_5v" and "ESP32"
4. Connections between "gps_neo6m" and "ESP32"
5. Connections between "r_acs_top_10k" and "ESP32"
6. Connections between "r_acs_bottom_22k" and "ESP32"
7. Connections between "r_batt_top_180k" and "ESP32"
8. Connections between "r_batt_bottom_22k" and "ESP32"
9. Connections between "zener_3v3" and "ESP32"
10. Connections between "cap_filter_1uf" and "ESP32"
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 & 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.




