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

Luồng dữ liệu: ESP32 gửi POST JSON tới Apps Script tại script.google.com, script ghi một dòng vào bảng tính, máy chủ chuyển hướng 302 sang script.googleusercontent.com, ESP32 đi theo và nhận chữ OK
ESP32 gửi JSON, Apps Script ghi một dòng rồi trả chữ OK qua một lần chuyển hướng (hình minh họa).
  1. 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.
  2. 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.
  3. 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.
  4. 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ị

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

Sơ đồ cầu phân áp: cực dương pin 18650 qua điện trở R1 100 kΩ, điểm giữa nối GPIO35 của ESP32, qua R2 100 kΩ xuống GND; cực âm pin nối GND chung
Cầu phân áp hai điện trở 100 kΩ đưa nửa điện áp pin vào GPIO35 (hình minh họa).

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

Năm bước triển khai Apps Script: tạo bảng tính, mở Apps Script, triển khai loại ứng dụng web, quyền truy cập bất kỳ ai, chép URL kết thúc bằng exec
Năm bước triển khai Apps Script thành web app (hình minh họa).
  1. Tạo một bảng tính mới trên Google Sheets.
  2. 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 KHOA thành chuỗi riêng của mình. Bấm biểu tượng lưu.
  3. 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).
  4. 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).
  5. Chép URL ứng dụng web (kết thúc bằng /exec) dán vào hằng URL_APPS_SCRIPT củ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ả

Bảng tính Du lieu với các cột Thời gian, Thiết bị, Lần gửi, Pin mV, Wi-Fi dBm, Đã chạy giây, mỗi phút một dòng
Trang tính sau vài phút chạy (số liệu ví dụ, hình minh họa).
Serial Monitor: mỗi lần gửi in điện áp pin, RSSI, HTTP 200 và chữ OK; hai lỗi SAI KHOA và read Timeout
Serial Monitor ở tốc độ 115200 (số liệu ví dụ).

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_BI khá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 appendRow của Apps Script, nhớ triển khai lại.

9. Xử lý lỗi thường gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
Trả về "SAI KHOA"Chuỗi KHOA ở ESP32 và Apps Script khác nhauChé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ậpSửa quyền truy cập trong Quản lý các lần triển khai
HTTP 302, không có chữ OKThiếu dòng setFollowRedirectsGiữ 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ếuTă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 scriptChưa triển khai phiên bản mớiTriển khai lại như mục 4
Điện áp pin bằng 0 hoặc nhảy lung tungCầ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-FiSai mật khẩu hoặc modem chỉ phát 5 GHzXem bài kết nối Wi-Fi trên ESP32

10. Xem thêm


Tin tức liên quan

Phát hiện chuyển động bằng cảm biến PIR HC-SR501 với ESP32: tự bật đèn qua relay
Phát hiện chuyển động bằng cảm biến PIR HC-SR501 với ESP32: tự bật đèn qua relay

Hướng dẫn dùng cảm biến chuyển động PIR HC-SR501 với ESP32: nguyên lý, cách chỉnh module, đọc tín hiệu bằng ngắt và ghép relay làm đèn tự bật khi có người, tự tắt sau 60 giây.

LCD I2C không hiện chữ, chỉ hiện ô vuông, sai địa chỉ 0x27/0x3F: tìm lỗi từng bước
LCD I2C không hiện chữ, chỉ hiện ô vuông, sai địa chỉ 0x27/0x3F: tìm lỗi từng bước

LCD 1602, LCD 2004 I2C không hiện chữ, chỉ hiện hàng ô vuông đen hoặc sai địa chỉ 0x27/0x3F: tìm lỗi nguồn, tương phản, dây I2C, địa chỉ và chương trình kiểm tra tự động.

Hướng dẫn bắt đầu với Wemos D1 R2 (ESP8266) trên Arduino IDE
Hướng dẫn bắt đầu với Wemos D1 R2 (ESP8266) trên Arduino IDE

Hướng dẫn dùng Wemos D1 R2 (ESP8266 khung Arduino UNO) với Arduino IDE 2.x: sơ đồ chân, cài driver CH340 và gói board ESP8266, nạp code nháy LED, kết nối WiFi và xử lý lỗi thường gặp.

Phased array siêu âm 40 kHz với ESP32 (phần 2): mạch đệm 12 V, đầu thu và quét búp sóng
Phased array siêu âm 40 kHz với ESP32 (phần 2): mạch đệm 12 V, đầu thu và quét búp sóng

Phần 2 của loạt phased array siêu âm: thêm mạch đệm 12 V bằng hai module L298N, lắp mạch thu dùng LM358, đo biên độ 40 kHz bằng ADC của ESP32 và chạy chương trình tự quét góc lái để xem búp sóng.

Giữ tốc độ động cơ DC ổn định bằng PID trên ESP32: encoder, chỉnh Kp Ki Kd, Serial Plotter
Giữ tốc độ động cơ DC ổn định bằng PID trên ESP32: encoder, chỉnh Kp Ki Kd, Serial Plotter

Hướng dẫn điều khiển vòng kín tốc độ động cơ DC bằng PID trên ESP32: đo tốc độ bằng encoder và PCNT, chu kỳ 20 ms, chống tích phân bão hòa, chỉnh Kp Ki Kd và thí nghiệm STEM.

Hướng dẫn ESP32 điều khiển module relay 4 kênh an toàn
Hướng dẫn ESP32 điều khiển module relay 4 kênh an toàn

Hướng dẫn ESP32 điều khiển relay 4 kênh: chọn GPIO an toàn, cấp nguồn 5 V phù hợp, xác định active LOW và lưu ý khi đóng cắt tải.

Hướng dẫn bắt đầu với Arduino Nano CH340: sơ đồ chân, driver và nạp code
Hướng dẫn bắt đầu với Arduino Nano CH340: sơ đồ chân, driver và nạp code

Hướng dẫn bắt đầu với Arduino Nano CH340: sơ đồ chân, cài driver, chọn Board và Processor, nạp Blink, in ra Serial Monitor, quét I2C trên A4/A5, cấp nguồn và xử lý lỗi thường gặp.

Hướng dẫn dùng màn hình LCD 2004 I2C với ESP32
Hướng dẫn dùng màn hình LCD 2004 I2C với ESP32

Hướng dẫn nối màn hình LCD 2004 I2C (mạch PCF8574) với ESP32 DevKit V1, quét địa chỉ I2C, hiển thị 4 dòng chữ, tạo ký tự riêng, làm bộ đếm thời gian và đổi sang LCD 1602.


Bình luận
Đã thêm vào giỏ hàng