Ghi dữ liệu ESP32 lên Google Sheets bằng Apps Script: điện áp pin, Wi-Fi, không cần máy chủ riêng
Tạo bảng tính, triển khai Apps Script thành web app, đo pin 18650 qua cầu phân áp và chương trình ESP32 gửi JSON lên Google Sheets mỗi phút, kèm bảng xử lý lỗi.
Muốn lưu lại số liệu đo của ESP32 để xem về sau (điện áp pin qua nhiều ngày, nhiệt độ kho, số lần mở cửa) mà không phải dựng máy chủ riêng, cách đơn giản là ghi thẳng vào một bảng tính Google Sheets. Một đoạn Apps Script nhỏ gắn với bảng tính đóng vai "máy chủ": ESP32 gửi gói JSON qua HTTPS, script ghi thêm một dòng kèm thời gian. Bài này hướng dẫn tạo bảng tính, triển khai Apps Script thành web app, đo điện áp pin 18650 bằng cầu phân áp và chương trình ESP32 gửi dữ liệu mỗi phút.
1. Dữ liệu đi từ ESP32 vào bảng tính thế nào

- ESP32 gửi yêu cầu POST chứa gói JSON tới địa chỉ web app dạng
https://script.google.com/macros/s/…/exec. - Hàm
doPost(e)trong Apps Script đọc gói JSON, kiểm tra khóa, ghi một dòng mới vào trang tính. - Vì lý do bảo mật, Google không trả kết quả ngay tại script.google.com mà chuyển hướng (mã 302) sang một địa chỉ dùng một lần ở script.googleusercontent.com.
- ESP32 đi theo chuyển hướng bằng lệnh GET và nhận chữ
OK.
Cột thời gian do Apps Script ghi bằng giờ của máy chủ Google lúc nhận, nên ESP32 không cần đồng hồ thời gian thực hay lấy giờ Internet.
2. Chuẩn bị
- ESP32 DevKit V1 30 chân cấp nguồn bằng pin qua mô đun pin 18650, hoặc mạch ESP32 tích hợp pin 18650.
- Hai điện trở 100 kΩ để làm cầu phân áp đo pin; khay pin 18650 nếu dùng pin rời.
- Breadboard, dây cắm.
- Một tài khoản Google.
Không có pin cũng làm được: bỏ phần cầu phân áp, chương trình vẫn gửi cường độ Wi-Fi và thời gian chạy; cột pin khi đó chỉ là số đọc nhiễu, bỏ qua.
3. Mạch đo điện áp pin

Pin Li-ion 18650 đầy khoảng 4,2 V, cao hơn mức ADC của ESP32 đo tốt. Hai điện trở bằng nhau chia đôi điện áp: pin 4,2 V thì GPIO35 nhận 2,1 V. Chương trình đọc điện áp chân rồi nhân 2. Hai điện trở 100 kΩ chỉ hao khoảng 21 µA, nhỏ so với dòng của ESP32. Cách đo, hiệu chỉnh và sai số của ADC xem bài đo điện áp pin 18650 bằng ADC.
Với mạch ESP32 tích hợp pin, xem mạch đã có sẵn cầu đo pin nối vào chân nào chưa; nếu chưa, hàn cầu phân áp từ cực dương đế pin như hình. Cách cấp nguồn pin cho ESP32 xem bài cấp nguồn ESP32 bằng pin 18650.
4. Tạo bảng tính và triển khai Apps Script

- Tạo một bảng tính mới trên Google Sheets.
- Mở Tiện ích mở rộng > Apps Script (Extensions > Apps Script). Xóa đoạn mã mẫu, dán đoạn mã bên dưới. Đổi chuỗi
KHOAthành chuỗi riêng của mình. Bấm biểu tượng lưu. - Bấm Triển khai > Tùy chọn triển khai mới (Deploy > New deployment), bấm biểu tượng bánh răng chọn loại Ứng dụng web (Web app).
- Mục Thực thi dưới danh nghĩa chọn Tôi; mục Người có quyền truy cập chọn Bất kỳ ai. Bấm Triển khai, cấp quyền cho tài khoản khi được hỏi (Google cảnh báo ứng dụng chưa được xác minh vì đây là script tự viết; chọn tiếp tục với tài khoản của mình).
- Chép URL ứng dụng web (kết thúc bằng
/exec) dán vào hằngURL_APPS_SCRIPTcủa chương trình ESP32.
// PYWORLD.VN - Apps Script nhận dữ liệu ESP32 gửi lên và ghi thêm một dòng vào trang tính
// Dán vào Tiện ích mở rộng > Apps Script của bảng tính, rồi Triển khai > Tùy chọn triển khai mới > Ứng dụng web
// Mỗi dòng gồm thời gian của máy chủ Google và các giá trị trong gói JSON của ESP32
const KHOA = 'doi-chuoi-nay'; // Phải trùng hằng KHOA trong chương trình ESP32
const TEN_TRANG = 'Du lieu'; // Tên trang tính sẽ ghi; chưa có thì tự tạo
function doPost(e) {
let d;
try {
d = JSON.parse(e.postData.contents);
} catch (loi) {
return ContentService.createTextOutput('SAI JSON');
}
if (d.khoa !== KHOA) {
return ContentService.createTextOutput('SAI KHOA');
}
const bang = SpreadsheetApp.getActiveSpreadsheet();
const trang = bang.getSheetByName(TEN_TRANG) || bang.insertSheet(TEN_TRANG);
if (trang.getLastRow() === 0) {
trang.appendRow(['Thời gian', 'Thiết bị', 'Lần gửi', 'Pin (mV)', 'Wi-Fi (dBm)', 'Đã chạy (giây)']);
}
trang.appendRow([new Date(), d.thiet_bi, d.lan, d.pin_mv, d.rssi, d.giay]);
return ContentService.createTextOutput('OK');
}
// Mở địa chỉ Web app bằng trình duyệt để kiểm tra đã triển khai đúng chưa
function doGet(e) {
return ContentService.createTextOutput('Web app dang chay. ESP32 gui du lieu bang POST.');
}
"Bất kỳ ai" nghĩa là ai biết địa chỉ cũng gửi được dữ liệu, nên script kiểm tra trường khoa trong gói JSON: sai khóa thì trả "SAI KHOA" và không ghi. Mở URL web app bằng trình duyệt thấy dòng "Web app dang chay" là đã triển khai đúng.
Sửa script xong phải triển khai lại: vào Triển khai > Quản lý các lần triển khai, sửa lần triển khai đang dùng, chọn Phiên bản mới. Chỉ bấm lưu thì web app vẫn chạy mã cũ, và địa chỉ /exec giữ nguyên.
5. Chương trình ESP32
Cài thư viện ArduinoJson (Benoit Blanchon, bản 7.x) bằng Library Manager. Điền Wi-Fi 2,4 GHz, URL web app, khóa trùng với Apps Script rồi nạp. Bài dùng gói esp32 phiên bản 3.3.10 và ArduinoJson 7.4.3.
// PYWORLD.VN - Ghi dữ liệu ESP32 lên Google Sheets qua Apps Script, mỗi phút một dòng
// Bo mạch: ESP32 DevKit V1 hoặc mạch ESP32 tích hợp pin 18650; cầu phân áp 100 kΩ / 100 kΩ đo pin ở GPIO35
// ESP32 gửi JSON bằng HTTPClient (HTTPS); Apps Script nhận rồi ghi thêm một dòng, kèm thời gian của máy chủ Google
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
const char *TEN_WIFI = "TEN_WIFI";
const char *MAT_KHAU_WIFI = "MAT_KHAU_WIFI";
const char *URL_APPS_SCRIPT = "https://script.google.com/macros/s/MA_TRIEN_KHAI/exec"; // Địa chỉ Web app
const char *KHOA = "doi-chuoi-nay"; // Phải trùng hằng KHOA trong Apps Script
const char *TEN_THIET_BI = "esp32-pin-01"; // Tên để phân biệt khi nhiều mạch cùng ghi một bảng
const uint8_t CHAN_PIN = 35; // GPIO35: chỉ đọc, có ADC
const float HE_SO_CAU = 2.0; // Cầu 100 kΩ / 100 kΩ: điện áp pin = điện áp chân x 2
const unsigned long CHU_KY_GUI = 60000; // Gửi mỗi 60 giây
WiFiClientSecure tls;
unsigned long lanGui = 0;
uint32_t soLan = 0;
bool chuaGui = true;
// Đọc 16 lần lấy trung bình, đổi ra điện áp pin (mV)
uint32_t docPinMv() {
uint32_t tong = 0;
for (uint8_t i = 0; i < 16; i++) tong += analogReadMilliVolts(CHAN_PIN);
return (uint32_t)(tong / 16 * HE_SO_CAU);
}
bool guiDuLieu() {
uint32_t pinMv = docPinMv();
JsonDocument doc;
doc["khoa"] = KHOA;
doc["thiet_bi"] = TEN_THIET_BI;
doc["lan"] = soLan;
doc["pin_mv"] = pinMv;
doc["rssi"] = WiFi.RSSI();
doc["giay"] = millis() / 1000;
String than;
serializeJson(doc, than);
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); // Apps Script trả kết quả qua một lần chuyển hướng 302
http.setTimeout(15000); // Google có thể mất vài giây mới trả lời
http.begin(tls, URL_APPS_SCRIPT);
http.addHeader("Content-Type", "application/json");
int ma = http.POST(than);
String traVe = (ma > 0) ? http.getString() : HTTPClient::errorToString(ma);
http.end();
traVe.trim();
Serial.printf("Lan %u: pin %u mV, RSSI %d dBm -> HTTP %d, tra ve: %s\n", (unsigned)soLan, (unsigned)pinMv,
(int)WiFi.RSSI(), ma, traVe.c_str());
return ma == 200 && traVe == "OK";
}
void setup() {
Serial.begin(115200);
tls.setInsecure(); // Có mã hóa nhưng không kiểm tra chứng chỉ máy chủ (xem giải thích trong bài)
WiFi.mode(WIFI_STA);
WiFi.begin(TEN_WIFI, MAT_KHAU_WIFI);
Serial.println("Dang ket noi Wi-Fi...");
}
void loop() {
if (WiFi.status() != WL_CONNECTED) return;
if (chuaGui || millis() - lanGui >= CHU_KY_GUI) {
if (chuaGui) Serial.printf("Wi-Fi OK, IP %s\n", WiFi.localIP().toString().c_str());
chuaGui = false;
lanGui = millis();
soLan++;
guiDuLieu();
}
}
6. Giải thích các phần chính
Đi theo chuyển hướng
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS) bảo HTTPClient đi theo chuyển hướng. Với mã 302, thư viện đổi sang GET và bỏ phần thân, đúng như Google mong đợi. Bỏ dòng này thì ESP32 dừng ở mã 302: dữ liệu vẫn được ghi nhưng chương trình không biết kết quả.
Chờ đủ lâu
Apps Script thường trả lời trong 1–3 giây, có lúc chậm hơn khi script vừa "thức dậy". Thời gian chờ mặc định của HTTPClient ngắn, nên chương trình đặt http.setTimeout(15000).
Đọc pin bằng analogReadMilliVolts()
Hàm này trả về điện áp tại chân (mV) đã bù theo hệ số hiệu chỉnh lưu trong chip, chính xác hơn tự đổi từ giá trị thô. Chương trình đọc 16 lần lấy trung bình rồi nhân hệ số cầu 2, nên số trong bảng luôn là số chẵn.
Mã hóa
Kết nối là HTTPS. tls.setInsecure() bỏ qua kiểm tra chứng chỉ để chương trình gọn, đổi lại kém an toàn trước mạng Wi-Fi không tin cậy. Thiết bị dùng lâu dài nên nạp chứng chỉ gốc bằng tls.setCACert().
Bao lâu thì đầy bảng
Một bảng tính Google Sheets chứa được khoảng 10 triệu ô. Mỗi dòng ở đây có 6 ô, gửi mỗi phút một dòng thì hơn 3 năm mới đầy. Apps Script cũng có hạn mức dùng hằng ngày: gửi mỗi phút từ vài mạch là ổn, không nên gửi mỗi giây.
7. Kết quả


Có dữ liệu rồi, chọn cột A và D trong bảng tính, Chèn > Biểu đồ để vẽ đồ thị điện áp pin theo thời gian. Bảng tính tự cập nhật khi có dòng mới; mở trên điện thoại cũng xem được.
8. Mở rộng
- Nhiều mạch, một bảng: mỗi mạch đặt
TEN_THIET_BIkhác nhau, dùng chung URL và khóa. Lọc theo cột B để xem từng mạch. - Chạy pin lâu: ESP32 giữ Wi-Fi liên tục tốn khoảng 80–150 mA. Muốn chạy pin nhiều ngày, cho ESP32 ngủ sâu giữa hai lần gửi như bài Deep Sleep tiết kiệm pin; bài vẽ đồ thị trên ThingSpeak cùng loạt làm đúng việc này.
- Cảnh báo: pin xuống dưới ngưỡng thì gửi thêm tin Telegram như bài gửi cảnh báo qua Telegram cùng loạt.
- Thêm cảm biến: thêm trường vào gói JSON và thêm cột trong
appendRowcủa Apps Script, nhớ triển khai lại.
9. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Trả về "SAI KHOA" | Chuỗi KHOA ở ESP32 và Apps Script khác nhau | Chép cùng một chuỗi cho cả hai, triển khai lại script nếu vừa sửa |
Trả về một trang HTML (bắt đầu bằng <!DOCTYPE html>) | Web app chưa để "Bất kỳ ai", Google đòi đăng nhập | Sửa quyền truy cập trong Quản lý các lần triển khai |
| HTTP 302, không có chữ OK | Thiếu dòng setFollowRedirects | Giữ nguyên dòng đó như chương trình mẫu |
| HTTP -11, "read Timeout" | Google trả lời chậm hơn thời gian chờ, hoặc Wi-Fi yếu | Tăng setTimeout, xem RSSI; lần gửi sau thường thành công |
| Bảng không có cột mới vừa thêm vào script | Chưa triển khai phiên bản mới | Triển khai lại như mục 4 |
| Điện áp pin bằng 0 hoặc nhảy lung tung | Cầu phân áp chưa nối, nối nhầm chân, quên nối GND chung | Đo bằng đồng hồ: điểm giữa cầu phải bằng nửa điện áp pin |
| Không vào được Wi-Fi | Sai mật khẩu hoặc modem chỉ phát 5 GHz | Xem bài kết nối Wi-Fi trên ESP32 |
10. Xem thêm
- Đo điện áp pin 18650 bằng ADC của ESP32
- Mạch ESP32 tích hợp OLED 0.96 và pin 18650
- Gửi dữ liệu bằng MQTT khi cần nhận dữ liệu tức thời
- Các bài cùng loạt: vẽ đồ thị trên ThingSpeak, gửi cảnh báo qua Telegram, lưu dữ liệu lên Firebase.
Xem thêm