Lập trình hướng đối tượng kết nối WiFi cơ bản cho ESP32
Dựng bộ khung firmware nhiều file cho board IoT, đóng gói phần WiFi thành một class riêng, lấy số MAC của ESP32 làm ID thiết bị và kết nối vào mạng WiFi 2.4 GHz.
- Tạo sketch và tách chương trình thành nhiều file
.h/.cppthay vì viết hết trong file.ino. - Hiểu và dùng macro chống include lặp (include guard) trong mỗi file header.
- Khai báo class MyWIFI chứa biến và hàm phục vụ kết nối mạng.
- Lấy số MAC của ESP32 — đây chính là ID để IoT-Cloud nhận diện board.
- Kết nối WiFi có thời gian chờ và in trạng thái ra Serial Monitor.
-
Tạo thư mục và file sketch
Tạo một folder tên
AIoTSemi. Mở Arduino IDE 2.x, chọn File → New Sketch, bấm nút … ở góc phải thanh tab → Rename để đổi tên file mặc định thànhAIoTSemi.ino, rồi lưu vào folder vừa tạo.Lưu ý: file.inobắt buộc nằm trong folder cùng tên — ở đây làAIoTSemi/AIoTSemi.ino, nếu không Arduino IDE sẽ không mở được sketch. -
Viết và nạp chương trình đầu tiên
Gõ đoạn code dưới đây để kiểm tra toàn bộ chuỗi công cụ đã chạy được: soạn thảo → biên dịch → nạp → xem kết quả.
- Chọn board: Tools → Board → esp32 → ESP32 Dev Module.
- Chọn cổng: Tools → Port → cổng COM của KIT (nhớ cắm cáp USB vào laptop).
- Nút ✓ để biên dịch, nút → để nạp, nút kính lúp để mở Serial Monitor.
-
Dựng bộ khung chương trình
Bấm nút … → New Tab → nhập tên file, lần lượt tạo 3 file:
ChuongTrinhChinh.cpp,ChuongTrinhChinh.h,AIoTSemi.h. Mỗi file.hđều phải mở đầu bằng bộ macro chống include lặp.Quy ước: tên macro đặt theo tên file —#ifndef _TenFile_h,#define _TenFile_h, … và kết thúc bằng#endif. Nhờ vậy nội dung header chỉ được biên dịch một lần dù bị gọi ở nhiều nơi. -
Khai báo hàm ở file .h, viết thân hàm ở file .cpp
File header chỉ chứa tên hàm. Toàn bộ nội dung thực thi nằm trong file
.cpp— copy luôn đoạn code đã viết trongAIoTSemi.inosang cho nhanh. -
Gọi hàm từ file .ino
File
.inotừ nay chỉ còn hai lời gọi hàm:KhoiTao()chạy một lần trongsetup(),ChayChuongTrinhChinh()chạy lặp trongloop().Kiểm tra: nạp lại code cho ESP32, nếu Serial Monitor vẫn in raHello IoTer's worldnghĩa là bộ khung đã đúng. -
Tạo cặp file MyWIFI.cpp và MyWIFI.h
Tạo thêm hai tab mới để chứa toàn bộ code kết nối WiFi. Trong
MyWIFI.htạo macro mặc định như các header khác, cònMyWIFI.cppmở đầu bằng lời gọi#include "MyWIFI.h".Bắt buộc: tên file MyWIFI để không trùng với thư việnWiFi.hcó sẵn của ESP32. -
Khai báo class WIFI
Đây là phần "hướng đối tượng" của bài: gom biến thông tin mạng và các hàm thao tác WiFi vào chung một class.
- Tiền tố
publiccho phép các hàm, các biến trong class được gọi từ bên ngoài class. - ESP32 chỉ hỗ trợ WiFi băng tần 2,4 GHz — không kết nối được mạng 5 GHz.
- Tiền tố
-
Viết hàm lấy số MAC
Số MAC là chuỗi định danh duy nhất của mỗi con ESP32. Hàm bỏ hết dấu
:để có chuỗi ID gọn, sau này dùng làm mã thiết bị trên IoT-Cloud. -
Gọi hàm LaySoMAC từ chương trình chính
Khai báo biến toàn cục
_IDđể giữ số MAC và biến đối tượng_WiFiđể gọi các hàm public của class.Ghi lại số MAC này. Ở bài 02 bạn sẽ nhập đúng chuỗi đó vào app AIoTSemi để app nhận dữ liệu từ board của mình. -
Viết hàm kết nối WiFi
Hàm nhận vào số giây tối đa được phép chờ. Vòng lặp kiểm tra trạng thái mỗi giây và in số đếm ra màn hình để biết board đang cố kết nối.
-
Kết nối WiFi và kiểm tra trạng thái
Bổ sung thư viện
<MyWIFI.h>vào đầuChuongTrinhChinh.cpp, gọi hàm kết nối trongKhoiTao()và in trạng thái mạng trong vòng lặp chính.Vì sao phải có thời gian chờ? Nếu để ESP32 chờ kết nối vô hạn, board sẽ bị watchdog reset và không thể phát Access Point khi cần cấu hình lại mạng.
Lập trình gửi dữ liệu cảm biến lên IoT-Cloud và hiển thị trên app
Viết class POSTGET để board đẩy dữ liệu lên server qua API, đóng gói dữ liệu theo đúng định dạng chuỗi của IoT-Cloud, chạy tác vụ gửi dữ liệu trên một core riêng của ESP32 và xem kết quả trên app AIoTSemi.
- Dùng thư viện HTTPClient để gửi request
POSTdạng JSON lên server. - Chuẩn hóa chuỗi dữ liệu theo format mà API của IoT-Cloud quy định.
- Tạo task FreeRTOS ghim vào core 1, có mutex khóa không cho POST/GET chạy cùng lúc.
- Cài app AIoTSemi, tạo tài khoản và thêm mã thiết bị (số MAC) để xem dữ liệu.
- Kiểm thử API bằng Postman trước khi nạp firmware.
-
Tạo cặp file POSTGET.cpp và POSTGET.h
Bấm nút … → New Tab để tạo hai file mới chứa toàn bộ code POST và GET data giữa IoT-Device và IoT-Cloud. Tạo macro mặc định trong
POSTGET.h, và gọi#include "POSTGET.h"ở đầuPOSTGET.cppđể dùng được các biến/hàm khai báo trong file.h. -
Khai báo class POSTGET
Biến
httpchỉ dùng nội bộ nên đểprivate; hàmPOSTDuLieuBoardđược gọi từ bên ngoài nên đểpublic. -
Viết hàm POST dữ liệu lên cloud
Hàm chỉ gửi khi WiFi đang kết nối. Dữ liệu được gói thành chuỗi JSON hai trường:
IDlà số MAC của board,Slà chuỗi dữ liệu đã chuẩn hóa.JSON là gì? Là định dạng văn bản dạng{"khóa": "giá trị"}dùng để trao đổi dữ liệu giữa thiết bị và server — đây chính là chuỗi mà dòngString data = …đang tạo ra. -
Khai báo thêm hàm gửi dữ liệu trong chương trình chính
Bổ sung một hàm thực hiện tác vụ (task) gửi dữ liệu lên IoT-Cloud vào file header của chương trình chính.
-
Hiểu định dạng chuỗi dữ liệu gửi lên cloud
API của IoT-Cloud nhận một chuỗi duy nhất theo cấu trúc
K1K2K3MODERSSI;NhietDo;DoAm;RTC. Ví dụ chuỗi100077;23.5;82.9;12:26:13 12/04/2024được đọc như sau:Biến Trường Ý nghĩa Ví dụ s00 K1 Relay 1 đang ON (1) hay OFF (0) 1 s01 K2 Trạng thái relay 2 0 s02 K3 Trạng thái relay 3 0 s03 MODE 0 = MANUAL (điều khiển bằng tay) 0 s04 RSSI Độ mạnh WiFi tính theo % 77 s1 NhietDo Nhiệt độ, đơn vị °C 23.5 s2 DoAm Độ ẩm, đơn vị %RH 82.9 s3 RTC Thời gian thực, format hh:mm:ss DD/MM/YYYY 12:26:13 12/04/2024 Đúng thứ tự: 5 ký tự đầu dính liền nhau không có dấu phân cách; ba trường còn lại ngăn nhau bằng dấu;. Riêng chuỗi thời gian phải để giờ trước, ngày sau. -
Viết hàm POST dữ liệu trong chương trình chính
Ở bài này board chưa đọc cảm biến thật nên các giá trị được đặt cứng để kiểm tra đường truyền trước. Nhớ khai báo biến toàn cục
POSTGET _POSTGET;và thêm#include "POSTGET.h"ở đầu file. -
Tạo task gửi dữ liệu chạy trên core riêng
ESP32 có 2 core, nên tách hẳn việc gửi dữ liệu thành một task riêng để không làm nghẽn chương trình chính. Biến đếm
_tickPOSTDuLieutăng mỗi 1 ms; khi đếm đủ ngưỡng thì gọi hàm POST một lần rồi reset về 0.Chu kỳ gửi: mỗi vòng lặp trễ 1 ms, nên ngưỡng6000≈ 6 giây một lần gửi. Muốn 1 phút thì đổi thành60000. -
Khởi tạo mutex và gắn task vào core 1
Đoạn code khởi tạo được đặt cuối hàm
ChayChuongTrinhChinh()để tạo khóa mutex và sinh task gửi dữ liệu. -
Cho số liệu thay đổi bằng hàm random
Nếu để giá trị cố định thì trên app không thấy dữ liệu nhúc nhích. Thay các con số giả lập bằng
random()để quan sát app cập nhật theo thời gian thực.Lưu ý: hàmrandom()chỉ trả về số nguyên, và phải chuyển kiểu từ số sang chuỗi bằng cú phápString(random(N)). -
Cài app AIoTSemi và tạo tài khoản
Tải app AIoTSemi trên App Store hoặc CH Play. Đăng nhập bằng tài khoản quản trị để mở màn hình đăng ký, sau đó tạo tài khoản riêng cho mình.
Trường Nhập gì Tài khoản ADMIN Username AISAdmin · password AISAdmin — chỉ dùng để vào màn hình đăng ký mới một tài khoản Tên đăng nhập Tên tài khoản mới muốn tạo Mật khẩu Mật khẩu của tài khoản mới Tên hiển thị Tên xuất hiện trên app sau khi đăng nhập Số điện thoại Dùng để truy vấn lại khi quên mật khẩu Bấm Đăng ký; nếu app báo thành công thì bấm Đăng nhập để vào app luôn.
-
Thêm mã thiết bị của board vào app
App chỉ hiển thị dữ liệu của những board đã được khai báo. Trình tự trên app:
- Từ màn hình chính, bấm nút bánh răng để sang màn hình Cài đặt.
- Bấm nút + để sang màn hình Quản lý danh sách board.
- Nhập số MAC của ESP32 (chuỗi in ra ở bài 01) vào ô nhập rồi bấm Thêm.
- Bấm nút quay lại hai lần để về màn hình chính — thẻ thông tin của board vừa thêm sẽ xuất hiện.
- Nút bút chì ở màn hình chính cho phép đặt mô tả và tên cho 3 kênh K1…K3.
Đối chiếu: mã thiết bị trên app phải trùng từng ký tự với chuỗiID:mà Serial Monitor in ra, ví dụA0B765D56648. -
Kiểm thử API bằng Postman
Trước khi nghi ngờ firmware, hãy thử bắn dữ liệu thẳng lên server bằng Postman (bản cài đặt hoặc chạy online) để chắc chắn API và mã thiết bị đều đúng.
Thiết lập Giá trị Method POST URL https://App.AIoTSemi.vn/api/TS3R_DuLieu Body raw → JSON Header Content-Type: application/json Chuỗi111177nghĩa là K1 = K2 = K3 = 1 (bật cả 3 relay), MODE = 1 và RSSI = 77. ThayIDbằng số MAC board của bạn để thấy dữ liệu chạy vào đúng thẻ thiết bị trên app. -
Ghép lại toàn bộ chương trình chính
Sau bài 02, file
ChuongTrinhChinh.cppcó thứ tự như dưới đây. Mở khối này để đối chiếu nếu code của bạn báo lỗi biên dịch.Xem trọn vẹn ChuongTrinhChinh.cpp
Lập trình lấy thời gian thực (RTC) từ IoT-Cloud của AIoTSemi
ESP32 không có đồng hồ chạy khi mất điện. Bài này viết class RTC gọi API của IoT-Cloud để xin thời gian thực qua HTTP GET, giúp board luôn biết đúng ngày giờ mà không cần phần cứng đồng hồ.
- Tạo cặp file RTC.cpp / RTC.h và khai báo class RTC riêng cho phần thời gian.
- Dùng HTTP GET để lấy chuỗi thời gian từ API
/api/ThoiGiancủa AIoTSemi. - Hiểu quy trình chuẩn của
HTTPClient:begin→GET→ kiểm tra mã 200 →getString→end. - Gọi hàm lấy RTC bên trong task gửi dữ liệu đã dựng ở bài 02.
-
Tạo cặp file RTC.cpp và RTC.h
Bấm nút … → New Tab tạo hai file mới để viết code lấy thời gian thực. Trong
RTC.htạo macro chống include lặp và khai báo class RTC; đầuRTC.cppgọi#include "RTC.h".Lưu ý: class ở đây tên làRTC(viết hoa) — trùng ý nghĩa nhưng không đụng thư viện nào của ESP32, nên dùng được. -
Khai báo hàm lấy RTC từ IoT-Cloud
Thêm khai báo hàm
publictrả vềbool: true nếu lấy được thời gian, false nếu thất bại. -
Viết hàm gọi API lấy thời gian
Hàm chỉ chạy khi WiFi đang kết nối. Server trả tối đa 512 byte, nên code kiểm tra kích thước payload trước khi đọc chuỗi. Nếu chuỗi trả về có độ dài lớn hơn 0 thì coi như lấy RTC thành công.
Mã 200 (HTTP_CODE_OK) là tín hiệu server trả dữ liệu thành công. Nếu nhận mã khác, hàm đóng kết nối và trả về false để chương trình chính biết mà xử lý. -
Khai báo biến RTC và gọi hàm trong chương trình chính
Trong
ChuongTrinhChinh.cpp, thêm#include "RTC.h", khai báo biến toàn cụcRTC _RTC;rồi gọi hàm lấy thời gian ngay trong task gửi dữ liệu đã dựng ở bài 02.Kết quả là chuỗi JSON: API trả về thứ, ngày, tháng, năm, giờ, phút, giây gói trong một object JSON. Bài 06 sẽ tách các trường này ra để chỉnh đồng hồ phần cứng.
Lập trình lấy thời gian thực (RTC) từ NTP Server
Ngoài IoT-Cloud của AIoTSemi, board còn có thể lấy giờ chuẩn từ NTP Server công cộng. Bài này thêm hàm thứ hai vào class RTC dùng thư viện NTPClient, tính múi giờ GMT+7 và tách sẵn ngày, tháng, năm, giờ, phút, giây.
- Cài và dùng hai thư viện NTPClient và WiFiUdp để hỏi giờ từ NTP.
- Cấu hình máy chủ
asia.pool.ntp.orgvà độ lệch múi giờ GMT+7 = 25200 giây. - Đổi epoch time (số giây từ 1970) thành ngày/tháng/năm bằng
gmtime. - Bổ sung hàm này song song với hàm lấy RTC từ IoT-Cloud đã viết ở bài 03.
-
Khai báo thêm hàm lấy RTC từ NTP
Mở lại
RTC.h, thêm một dòng khai báo hàm mới bên cạnh hàm cũ. -
Nhúng thư viện và cấu hình NTP Client
Ở đầu
RTC.cppthêm hai thư viện, tạo đối tượng UDP và mộttimeClienttrỏ tới máy chủ NTP châu Á với độ lệch múi giờ Việt Nam.25200 = 7 × 3600. Đây là số giây chênh giữa giờ Việt Nam (GMT+7) và giờ chuẩn UTC. Đổi máy chủ hay múi giờ khác thì sửa hai tham số này. -
Viết hàm lấy và bóc tách thời gian NTP
getEpochTime()trả về số giây từ năm 1970. Hàmgmtimechuyển số đó thành cấu trúc ngày tháng; nhớ cộngtm_mon + 1vàtm_year + 1900để ra tháng và năm đúng.Vì sao +1 và +1900? Cấu trúctmđếm tháng từ 0 và đếm năm kể từ 1900, nên phải cộng bù để ra tháng và năm quen thuộc. -
Gọi hàm lấy RTC từ NTP trong chương trình chính
Trong task ở
ChuongTrinhChinh.cpp, thêm dòng gọi hàm NTP ngay cạnh dòng gọi hàm lấy RTC từ IoT-Cloud.Chọn nguồn giờ nào? NTP cho giờ chuẩn quốc tế rất chính xác; IoT-Cloud của AIoTSemi tiện khi muốn đồng bộ giờ với server ứng dụng. Bạn có thể bật/tắt từng hàm tùy nhu cầu.
Lập trình lấy thời gian thực từ module RTC DS3231
DS3231 là IC đồng hồ thời gian thực có pin nuôi riêng, vẫn giữ giờ khi board mất điện và không cần mạng. Bài này định nghĩa kiểu dữ liệu ThoiGian, đọc giờ từ DS3231 qua giao thức I2C và lưu vào một biến dùng chung cho cả class.
- Cài thư viện DS3231 và nhúng vào class RTC.
- Định nghĩa
struct ThoiGiangom 7 trường: thứ, ngày, tháng, năm, giờ, phút, giây. - Phân biệt biến private
ds(nói chuyện với IC) và biến publicdt(chứa kết quả để bên ngoài đọc). - Đọc giờ từ DS3231 và cộng
2000để ra đúng số năm bốn chữ số.
-
Nhúng thư viện DS3231 và định nghĩa struct ThoiGian
Mở
RTC.h. Thêm thư viện DS3231 (cần cài trước), rồi định nghĩa một kiểu dữ liệu có cấu trúc để gom trọn một mốc thời gian vào một biến.struct là gì? Là cách gộp nhiều biến liên quan thành một khối. Thay vì truyền 7 biến rời rạc, ta truyền một biếnThoiGianduy nhất — gọn và dễ đọc. -
Bổ sung class RTC với biến ds, dt và hàm mới
Biến
dsđểprivatevì chỉ dùng nội bộ để đọc IC; biếndtđểpublicđể chương trình chính đọc được kết quả. Thêm khai báo hàmLayRTCTuDS3231. -
Viết hàm đọc giờ từ DS3231
Kết quả đọc được đổ thẳng vào biến public
dtđể bên ngoài class lấy ra dùng. IC trả năm dạng hai chữ số nên phải cộng 2000.ds và dt khác nhau:dslà "kênh" nói chuyện với chip DS3231;dtlà "hộp" đựng kết quả. Hàm đọc từdsrồi cất vàodt. -
Khởi tạo I2C và khai báo chân kết nối
DS3231 nói chuyện với ESP32 qua giao thức I2C (2 dây SDA và SCL). Trong
ChuongTrinhChinh.hnhúng thư việnWire.h; trongChuongTrinhChinh.cppkhai báo chân và khởi tạo I2C ở hàmKhoiTao().Sơ đồ chân: theo thiết kế phần cứng của board IoT, chân 21 là SDA và chân 22 là SCL. Mọi thiết bị I2C (DS3231, SHT3x, PCF8574) đều dùng chung hai dây này. -
Gọi hàm và hiển thị giờ từ DS3231
Trong task ở
ChuongTrinhChinh.cpp, gọi hàm rồi in ra Serial. Vì các trường củadtlà số, phải ép sangStringmới ghép chuỗi để hiển thị được.Lưu ý: ban đầu IC DS3231 chứa giờ mặc định của nhà sản xuất (số vô nghĩa như trên). Cần lập trình thiết lập thời gian ban đầu cho DS3231 — chính là nội dung bài 06.
Lập trình cập nhật RTC cho IC DS3231 theo RTC từ IoT-Cloud
Ghép ba nguồn giờ lại với nhau: board xin giờ chuẩn từ IoT-Cloud, so với giờ đang chạy trong DS3231, nếu lệch quá thì tự chỉnh lại đồng hồ phần cứng. Cuối cùng đóng gói giờ thực của board thành chuỗi để gửi lên cloud hiển thị trên app.
- Dùng thư viện Arduino_JSON để tách chuỗi JSON thời gian từ IoT-Cloud thành các trường.
- Viết hàm cài đặt giờ cho DS3231 và hàm so sánh sai lệch giữa DS3231 và Cloud.
- Tự động chỉnh DS3231 khi lệch quá 3 phút hoặc khác ngày/tháng/năm.
- Chuẩn hóa giờ của board thành chuỗi
HH:mm:ss DD/MM/YYYYđể POST lên cloud.
-
Khai báo thêm hai hàm cài đặt và so sánh RTC
Trong
RTC.h, thêm hai khai báo hàm: một hàm ghi giờ xuống DS3231, một hàm so giờ giữa DS3231 và Cloud rồi cập nhật nếu cần.Hai vai trò:CaiDatRTCChoDS3231là hàm ghi giờ xuống chip;SoSanhSaiLechRTCDeCapNhatDS3231là hàm kiểm tra — nếu DS3231 lệch với Cloud thì mới gọi hàm ghi. -
Nhúng thư viện Arduino_JSON
Chuỗi thời gian mà IoT-Cloud trả về là JSON. Thêm thư viện
Arduino_JSON.h(cần cài) vào đầuRTC.cppđể bóc tách chuỗi này. -
Bổ sung phần tách JSON vào hàm lấy RTC từ Cloud
Quay lại hàm
LayRTCTuServerAIoTSemiở bài 03. Ngay sau khi lấy được chuỗi, dùngJSON.parseđể tách từng trường rồi đổ vào biến publicdt. KiểuJSONVarnằm trong thư viện Arduino_JSON.Biến public dt đóng vai cầu nối: hàm lấy RTC từ Cloud giờ không chỉ in ra màn hình mà còn cất giờ vàodt, để các hàm khác trong class dùng lại — không cần gọi mạng thêm lần nữa. -
Viết hàm cài đặt giờ cho DS3231
Hàm lấy giờ đang chứa trong biến public
dt(vừa được nạp từ Cloud) rồi ghi từng trường xuống chip DS3231. -
Viết hàm so sánh sai lệch và cập nhật
Hàm này xin giờ chuẩn từ Cloud (nạp vào
dt), đọc giờ đang chạy trong DS3231 (nạp vào biến tạmt), rồi so hai bên. Chênh quá 3 phút hoặc khác ngày/tháng/năm thì gọi hàm cài đặt lại.Quy đổi ra phút để so:giờ × 60 + phútgộp giờ và phút thành một số phút duy nhất, giúp so sánh chênh lệch đơn giản hơn. -
Gọi hàm kiểm tra trong task chính
Trong task ở
ChuongTrinhChinh.cpp, thay các dòng lấy RTC riêng lẻ bằng lời gọi hàm so sánh — mỗi chu kỳ board tự kiểm tra và chỉnh DS3231 nếu lệch.Ý nghĩa: khi chạy, board thấy DS3231 lệch so với Cloud nên tự động ghi lại giờ chuẩn xuống chip. Từ đó board luôn giữ đúng giờ ngay cả khi sau này mất mạng. -
Chuẩn hóa chuỗi RTC để gửi về Cloud
Bài 02 dùng thời gian đặt cứng. Giờ viết hàm biến giờ thực của board thành đúng định dạng
HH:mm:ss DD/MM/YYYYmà IoT-Cloud yêu cầu, thêm số 0 ở đầu khi cần cho đủ hai chữ số.Vì sao thêm số 0? Giờ 9 phải hiển thị là09để định dạng luôn đủ hai chữ số. Nếu không, chuỗi9:5:3sẽ sai định dạng và app đọc lỗi. -
Dùng giờ thực khi POST dữ liệu lên Cloud
Bước cuối: trong hàm
POSTDuLieuVeCloudDeHienThiTrenAPP()của bài 02, thay dòng đặt cứng thời gian bằng lời gọi hàm chuẩn hóa vừa viết. Từ nay board luôn gửi giờ thực lên cloud.Hoàn tất chuỗi RTC: phần cuối chuỗi data giờ là thời gian thực19:22:45 28/09/2025lấy trực tiếp từ board, không còn là số đặt cứng. Trên app, thời gian máy chủ và thời gian trên board sẽ khớp nhau.
Lập trình lấy dữ liệu cảm biến SHT31 từ board đưa lên IoT-Cloud để hiển thị trên app
Đến đây board đã biết đúng giờ. Bài này gắn cảm biến nhiệt độ và độ ẩm SHT31 qua I2C, đóng gói thành class SHT3x, xử lý trường hợp cảm biến rớt kết nối, rồi thay số liệu giả lập ở bài 02 bằng số đo thật để app hiển thị nhiệt độ và độ ẩm sống.
- Cài thư viện Adafruit_SHT31 và tạo cặp file CambienSHT31.cpp / .h.
- Khai báo class SHT3x với hai biến public
NhietDo,DoAm. - Khởi tạo cảm biến ở địa chỉ I2C 0x44 và đọc nhiệt độ, độ ẩm.
- Bắt lỗi NAN khi cảm biến mất kết nối, trả về
-1để app báo cho người dùng biết. - Đưa số đo thật vào chuỗi POST thay cho
random()ở bài 02.
-
Tạo cặp file CambienSHT31.cpp và CambienSHT31.h
Bấm nút … → New Tab tạo hai file mới. Trong
CambienSHT31.htạo macro chống include lặp; đầuCambienSHT31.cppgọi#include "CambienSHT31.h".Cần cài trước: thư viện Adafruit SHT31 trong Library Manager của Arduino IDE. Thư viện này dùng cho cả dòng SHT30 và SHT31. -
Nhúng thư viện và khai báo địa chỉ, chân I2C
Cảm biến dùng chung hai dây I2C (SDA chân 21, SCL chân 22) đã khởi tạo ở bài 05. Địa chỉ mặc định của SHT31 trên bus I2C là
0x44.0x44 là gì? Mỗi thiết bị trên bus I2C có một địa chỉ riêng để ESP32 gọi đúng. SHT31 mặc định là0x44(một số module có thể chuyển sang 0x45 bằng cầu nối chân ADDR). -
Khai báo class SHT3x
Biến
sht31(đối tượng của thư viện Adafruit) đểprivatevì chỉ dùng nội bộ; hai biến kết quảNhietDo,DoAmđểpublicđể chương trình chính đọc ra.Vì sao dùngdouble? Nhiệt độ và độ ẩm là số thực (ví dụ 28.7 °C), nên phải dùng kiểu số thực để giữ được phần thập phân, không dùngint. -
Viết hàm khởi tạo cảm biến
Hàm gọi
sht31.begin()với địa chỉ I2C. Vì bài 05 đã khởi tạoWirenên ở đây chỉ cần truyền địa chỉ, không cần khai lại chân SDA/SCL. -
Viết hàm đọc nhiệt độ và độ ẩm
Khi cảm biến rớt kết nối, thư viện trả về
NAN(không phải số). Hàm kiểm tra bằngisnan(), nếu lỗi thì đặt cả hai giá trị về-1làm tín hiệu để app hiển thị "không có cảm biến".NAN = Not A Number. Đây là giá trị đặc biệt báo phép đọc thất bại. Không so sánh NAN bằng dấu==được — bắt buộc dùng hàmisnan(). -
Khai báo biến và khởi tạo cảm biến trong chương trình chính
Trong
ChuongTrinhChinh.cpp, thêm#include "CambienSHT31.h", khai báo biến toàn cụcSHT3x _SHT3x;rồi gọi hàm khởi tạo trongKhoiTao(), đặt sau lệnh khởi tạo I2C của bài 05. -
Đưa số đo thật vào chuỗi POST lên cloud
Bước cuối: trong
POSTDuLieuVeCloudDeHienThiTrenAPP(), gọi hàm đọc cảm biến rồi thay hai giá trịrandom()của bài 02 bằng nhiệt độ và độ ẩm thật.Kiểm tra trên app: mở app AIoTSemi, thẻ thiết bị của bạn giờ hiển thị nhiệt độ và độ ẩm sống (ví dụ 28.7 °C và 72.51 %). Rút cảm biến ra thì cả hai về -1 để báo mất kết nối.
Lập trình điều khiển ON/OFF các relay
Từ đọc cảm biến, board chuyển sang tác động ra thế giới thật: đóng ngắt relay. Bài này đóng gói phần relay thành class Relay, cấu hình chân ra kiểu OUTPUT, viết hàm bật/tắt cả cụm lẫn từng kênh, và chạy thử bằng một task tự đảo trạng thái mỗi 10 giây.
- Tạo cặp file Relay.cpp / .h và khai báo class Relay với 3 biến trạng thái
K1,K2,K3. - Cấu hình chân relay là OUTPUT và xuất tín hiệu
1/0để đóng/ngắt. - Viết hàm bật/tắt cả cụm và hàm ON/OFF từng kênh chỉ khi trạng thái thay đổi.
- Dùng
#pragma regionđể gấp/mở khối code cho dễ đọc. - Tạo task điều khiển chạy trên core 1 để kiểm nghiệm relay tự đảo trạng thái mỗi 10 giây.
-
Tạo cặp file Relay.cpp và Relay.h
Bấm nút … → New Tab tạo hai file mới để lập trình điều khiển ON/OFF các relay. Trong
Relay.htạo macro chống include lặp; đầuRelay.cppgọi#include "Relay.h". -
Khai báo chân relay và hằng số ON/OFF
Ba relay nối vào ba chân GPIO của ESP32 theo thiết kế phần cứng của board. Đặt tên hằng số cho các chân và cho hai mức tín hiệu để code dễ đọc và dễ sửa.
Lưu ý: chân kết nối relay thay đổi theo thiết kế phần cứng của board IoT. Nên đặt các chân bằng#definecho tường minh — đổi board chỉ cần sửa số ở đây, không phải dò khắp code. -
Khai báo class Relay
Ba biến
K1,K2,K3đểpublicđể lưu trạng thái ON/OFF hiện tại của từng relay, cho phần khác của chương trình đọc ra. Khai báo các hàm khởi tạo, bật/tắt cụm và ON/OFF từng kênh.Hai nhóm hàm:ONCacRole/OFFCacRoletác động cả 3 relay cùng lúc; ba hàmONOFFBangTayKxđiều khiển riêng từng kênh theo lệnh app gửi xuống (dùng ở bài 10). -
Viết hàm khởi tạo chân relay
Chân điều khiển relay phải đặt chế độ
OUTPUT— tức là ESP32 xuất tín hiệu ra chân này để đóng/ngắt relay (khác với chân đọc cảm biến là INPUT).OUTPUT nghĩa là gì? Chân được đặt để "phát" tín hiệu ra ngoài. ESP32 gửi mức1(bật) hoặc0(tắt) tới relay, thay vì "nghe" tín hiệu vào như chân cảm biến. -
Viết hàm bật và tắt cả cụm relay
digitalWritexuất mức tín hiệu ra chân. Sau khi đóng/ngắt, hàm cập nhật luôn ba biến trạng thái để nơi khác biết relay đang bật hay tắt. -
Viết hàm ON/OFF từng kênh (điều khiển bằng tay)
Hàm cho kênh K1 chỉ tác động khi trạng thái mới khác trạng thái cũ — tránh đóng/ngắt relay liên tục vô ích. Trả về
truenếu có thực thi,falsenếu không đổi gì. Khối code được bọc bằng#pragma region.Copy cho K2, K3: hai hàmONOFFBangTayK2vàONOFFBangTayK3viết tương tự — chỉ đổiK1 → K2/K3và_pinRelay1 → _pinRelay2/_pinRelay3.#pragma regionlà gì? Cú pháp giúp gấp/mở một khối code dài trong trình soạn thảo cho dễ đọc, hoàn toàn không ảnh hưởng đến việc biên dịch chương trình. -
Khai báo biến relay trong chương trình chính
Trong
ChuongTrinhChinh.cpp, thêm#include "Relay.h"và khai báo biến toàn cụcRelay _Relay;. -
Khởi tạo chân relay trong hàm KhoiTao()
Gọi hàm khởi tạo chân ở đầu hàm
KhoiTao(), rồi OFF ngay để đảm bảo các thiết bị đều tắt lúc board vừa cấp nguồn — tránh tình trạng relay bật bất ngờ khi khởi động.Vì sao đặt ở đầu? Chân relay phải được cấu hình OUTPUT và tắt ngay khi board mở máy, nếu không relay có thể bật ngẫu nhiên trong lúc chờ các phần khác khởi tạo — nguy hiểm với thiết bị điện thật. -
Tạo task kiểm nghiệm relay đảo trạng thái mỗi 10 giây
Thêm một task chuyên trách điều khiển relay. Biến đếm tăng mỗi 1 ms; đủ 10000 (≈ 10 giây) thì đảo cả cụm: đang bật thì tắt, đang tắt thì bật.
-
Gắn task vào core 1
Trong hàm
ChayChuongTrinhChinh(), tạo task điều khiển relay và ghim vào core 1, giống cách làm với task POST dữ liệu ở bài 02.Quan sát: sau mỗi 10 giây, cả 3 relay chuyển trạng thái ON ↔ OFF, có thể nghe tiếng "tách" của relay. Đây chỉ là task kiểm nghiệm — bài 09 và 10 sẽ thay bằng lệnh điều khiển thật từ cloud và app.Phân core về sau: khi có nhiều task, cần cân nhắc chia chúng lên hai core của ESP32 để board chạy tối ưu nhất.
Lập trình lấy lệnh điều khiển từ IoT-Cloud của AIoTSemi gửi xuống board
Đến giờ board mới chỉ gửi dữ liệu lên. Bài này mở chiều ngược lại: board hỏi cloud xem người dùng vừa bấm nút gì trên app. Vì giờ có hai luồng truy cập mạng (gửi lên và lấy về) chạy song song, bài giới thiệu mutex tĩnh để hai task không giẫm chân nhau.
- Thêm hàm GET lệnh vào class POSTGET, gọi API lấy lệnh điều khiển từ IoT-Cloud.
- Xử lý chuỗi trả về: loại bỏ ký tự
[và]để tách JSON. - Nâng cấp từ mutex thường sang mutex tĩnh (static) với ba hàm bao an toàn.
- Viết lại các task theo cơ chế
takeMutexSafe/giveMutexSafe. - Tạo task lắng nghe lệnh chạy nhanh (100 ms) để phản hồi tức thì khi người dùng bấm nút.
-
Khai báo hàm GET lệnh trong POSTGET.h
Mở lại
POSTGET.h(bài 02), thêm một hàm trả vềString— chính là chuỗi lệnh mà app gửi xuống thông qua cloud. -
Viết hàm GET lệnh trong POSTGET.cpp
Hàm dùng phương thức
GETđể hỏi API. Chuỗi trả về có dấu[ở đầu và]ở cuối (dạng mảng JSON), nên phải cắt bỏ hai ký tự này để còn lại object JSON thuần.Vì sao cắt[và]? Server trả về một mảng JSON dạng[{...}]. Bỏ hai dấu ngoặc vuông ở hai đầu để còn lại đúng một object{...}, giúp bước tách JSON sau này gọn hơn. -
Khai báo các hàm mới trong ChuongTrinhChinh.h
Thêm hàm lắng nghe lệnh và ba hàm quản lý mutex an toàn. Đây là các hàm cần cho lập trình đa nhiệm FreeRTOS để hai luồng POST và GET không xung đột.
Mutex là gì? Là "chìa khóa" chỉ có một, ai giữ được mới vào vùng dùng chung (ở đây là kết nối mạng). Nhờ vậy task POST và task GET không cùng lúc gọi HTTP, tránh xung đột và reset board. -
Chuyển sang mutex tĩnh (static)
Trong
ChuongTrinhChinh.cpp, thay biến mutex cũ bằng mutex tĩnh. Mutex tĩnh dùng vùng nhớ cấp phát sẵn nên không bao giờ bị mất, hiệu suất cao và an toàn hơn mutex thường.FreeRTOS có sẵn trong ESP32: không cần cài thêm thư viện, Arduino IDE đã tự nhúng. Các hàmxSemaphore...dùng được ngay. -
Viết ba hàm bao mutex an toàn
Bộ ba hàm này bọc mutex thành một lớp an toàn: tự khởi tạo nếu chưa có, kiểm tra NULL trước khi dùng, tránh lỗi runtime khi task chưa kịp khởi tạo mutex.
Hàm Vai trò initMutex() Tạo mutex nếu chưa có, dùng buffer cấp sẵn takeMutexSafe() Chiếm quyền dùng mutex trong khoảng timeout; trả về true nếu lấy được giveMutexSafe() Trả lại quyền sau khi xong; chỉ nhả khi mutex đã tồn tại -
Viết lại task POST theo mutex an toàn
Task gửi dữ liệu giờ phải xin mutex trước khi làm việc, và nhả mutex sau khi xong. Chu kỳ 30 giây/lần để tránh spam cloud; timeout chờ mutex là 3 giây vì việc cập nhật RTC không cần gấp.
Bắt buộc nhả mutex: sau khi xong tác vụ phải gọigiveMutexSafe(), nếu không task khác sẽ chờ vô hạn. Đây là lỗi kinh điển khi lập trình đa nhiệm. -
Tạo task lắng nghe lệnh từ app
Task này phải chạy nhanh để phản hồi ngay khi người dùng bấm nút. Timeout chờ mutex và chu kỳ lặp đều để 100 ms — bởi lệnh bấm trên app cần xuống board gần như tức thì.
Hai nhịp khác nhau: task POST 30 giây/lần (gửi định kỳ), task GET 100 ms/lần (phản hồi tức thì). Cùng dùng chung một mutex nên không bao giờ đụng nhau khi gọi mạng. -
Viết hàm nội dung lắng nghe lệnh
Hàm gọi
GETLenhGuiXuongBoardđể lấy chuỗi lệnh, rồi in ra để kiểm tra. Chuỗi này chứa trạng thái các nút, chế độ làm việc từng kênh và mã lệnh (CODE).Mã lệnh 11: khi người dùng bấm nút điều khiển trên app, mã lệnh (ví dụ11) sẽ đi kèm chuỗi JSON này xuống board. Bài 10 sẽ đọc mã đó để thật sự đóng/ngắt relay. -
Khởi tạo mutex đầu tiên và chuyển task xuống cuối KhoiTao()
Trong
KhoiTao(): gọiinitMutex()trước tiên để mutex sẵn sàng, rồi chuyển toàn bộ phần tạo task từChayChuongTrinhChinh()lên cuối hàmKhoiTao(). Task GET ghim vào core 0, task POST vào core 1.Thứ tự quan trọng: mutex phải khởi tạo trước khi tạo bất kỳ task nào, nếu không task chạy lên mà chưa có mutex sẽ gây lỗi. Stack size8192 = 1024 × 8— task POST cần nhiều bộ nhớ hơn vì xử lý JSON và HTTP. -
Thêm chốt kiểm tra mutex và dọn code cũ
Trong
ChayChuongTrinhChinh(), thêm đoạn kiểm tra định kỳ: nếu mutex vô tình bị mất thì tự khởi tạo lại. Đồng thời gỡ (hoặc comment) khối tạo task cũ vì đã chuyển lênKhoiTao().Vì sao kiểm tra trong loop? Đây là lưới an toàn: nếu vì lý do nào đó mutex bị xóa, chương trình tự phục hồi thay vì treo. Với thiết bị IoT chạy 24/7, những chốt phòng ngừa như vậy rất quan trọng.
Lập trình điều khiển ON/OFF relay trên board từ app
Đây là bài khép vòng: bấm nút trên app → cloud gửi lệnh xuống → board đóng/ngắt relay thật → board báo trạng thái mới về cloud để app hiển thị đúng. Bài 08 đã biết bật/tắt relay, bài 09 đã biết lấy lệnh từ cloud. Bài này ghép hai mảnh đó lại: đọc mã CODE trong lệnh, thực thi đúng kênh người dùng chọn, rồi "bắt tay" phản hồi để app và board luôn đồng bộ.
- Thêm hàm CheDoMANUAL vào class Relay để đóng/ngắt cả 3 kênh theo lệnh tay từ app.
- Thêm hàm phản hồi mã CODE vào POSTGET để báo cloud biết board đã nhận lệnh.
- Bóc tách chuỗi JSON lệnh: lấy trạng thái
K1/K2/K3và mãCODE. - Viết ThucThiTacVuTheoCODE để đóng/ngắt relay đúng nút bấm rồi phản hồi
CODE=0. - Đồng bộ hai chiều: relay vừa đổi trạng thái là POST lại ngay để app khớp với thực tế.
-
Bổ sung hàm CheDoMANUAL vào class Relay
Mở lại
Relay.h(bài 08), thêm một dòng khai báo hàm điều khiển ON/OFF cả 3 relay ở chế độ MANUAL (bằng tay). Hàm này gom ba hàmONOFFBangTayKxđã viết ở bài 08 thành một lời gọi duy nhất.Về sau còn chế độ AUTO: khi cần tự động ON/OFF theo nhiệt độ, độ ẩm hay theo giờ cài đặt, ta chỉ việc khai báo thêm một hàmCheDoAUTO(...)tương tự — cấu trúc class đã sẵn sàng để mở rộng. -
Viết hàm CheDoMANUAL trong Relay.cpp
Hàm gọi lần lượt ba hàm bật/tắt từng kênh, mỗi hàm trả về
truenếu kênh đó thật sự đổi trạng thái. Nếu bất kỳ kênh nào đổi thì hàm trả vềtrue— tín hiệu để chương trình chính biết cần cập nhật lại dữ liệu lên cloud.Vì sao cần biết "có đổi hay không"? Nếu người dùng bấm nút mà trạng thái vốn đã đúng (đang ON lại bấm ON), relay không cần đổi và cũng không cần POST lại data. Chỉ khi thật sự có thay đổi mới gửi dữ liệu — tiết kiệm băng thông và tránh spam cloud. -
Khai báo hàm phản hồi CODE trong POSTGET.h
Mở lại
POSTGET.h(bài 02, 09), thêm hàm gửi mã CODE phản hồi từ board lên cloud. Đây là bước "bắt tay": board báo cho cloud biết nó đã nhận được lệnh, tránh tình trạng lệnh gửi đi mà không rõ board có nhận được hay không."Bắt tay" (handshake): lệnh đi xuống mangCODE=11; board nhận xong sẽ phản hồiCODE=0lên lại. Cloud thấy CODE về 0 là biết board đã thực thi, coi như hai bên đã bắt tay thành công. -
Viết hàm phản hồi CODE trong POSTGET.cpp
Hàm dùng phương thức
POSTđẩy lên một chuỗi JSON gồm hai trường:ID(số MAC của board) vàS(chuỗi trạng thái kèm mã CODE). API đích do IoT-Cloud của AIoTSemi cung cấp sẵn.datalà chuỗi JSON: chứa đúng hai thông tinIDvàS, ví dụ{"ID":"F024F950D8E0","S":"1010000"}. Cloud đọcIDđể biết đúng board nào, đọcSđể cập nhật trạng thái và mã CODE. -
Nhúng thư viện JSON và khai báo 2 hàm mới trong ChuongTrinhChinh.h
Mở
ChuongTrinhChinh.h: thêm thư viện xử lý JSON (đã cài từ bài lấy RTC từ cloud) và khai báo hai hàm — một hàm thực thi tác vụ theo CODE, một hàm phản hồi CODE về cloud.Đã cài trước đó: thư việnArduino_JSONđược thêm từ bài lấy thời gian thực từ IoT-Cloud (dùng để bóc tách chuỗi RTC). Nay chương trình chính cũng cần nó để đọc chuỗi lệnh, nên khai báo#includeở file này. -
Khai báo mã CODE và các biến lệnh trong ChuongTrinhChinh.cpp
Mỗi loại thao tác trên app tương ứng một mã CODE. Ở đây quy ước
CODEUserChonONOFF = 11: khi người dùng bấm các nút ON/OFF, cloud sẽ gửi mã 11 xuống. Thêm ba biến toàn cục để lưu lệnh ON/OFF của từng kênh.Ba biến này giữ trạng thái ON/OFF hiện tại của 3 relay để các hàm khác cùng dùng. -
Nâng cấp hàm lắng nghe lệnh để bóc tách CODE
Đây là bản nâng cấp của
LangNgheLenhAppGuiXuongBoard()ở bài 09. Sau khi lấy được chuỗi lệnh, dùngJSON.parseđể tách trườngS, rồi cắt chuỗi lấy trạng thái từng kênh và mã CODE. Nếu CODE đúng là mã người dùng chọn ON/OFF thì gọi hàm thực thi.Chuỗi
Smã hóa toàn bộ lệnh theo từng vị trí ký tự. Ví dụ vớiS = "101011":Đoạn cắt Ý nghĩa Giá trị ("101011") S.substring(0,1) K1 1 (ON) S.substring(1,2) K2 0 (OFF) S.substring(2,3) K3 1 (ON) vị trí kế MODE 0 (MANUAL) S.substring(4) CODE 11 -
Viết hàm thực thi tác vụ theo CODE
Hàm dùng
switchđể chọn việc theo mã CODE. Với mã ON/OFF: gọiCheDoMANUALđóng/ngắt relay, phản hồiCODE=0về cloud, và chỉ khi relay thật sự đổi trạng thái mới POST dữ liệu mới lên để app hiển thị đúng.Đồng bộ hai chiều — mấu chốt của bài: ngay khi relay dưới board đổi trạng thái, board lập tức POST data mới lên cloud để nút trên app khớp đúng thực tế. Nhờ vậy tránh được cảnh nút app đang ON nhưng relay dưới board vẫn OFF (và ngược lại). -
Viết hàm phản hồi CODE về cloud
Hàm ghép chuỗi trạng thái ba relay hiện tại với mã CODE rồi gọi hàm POSTGET vừa viết ở bước 4 để đẩy lên cloud. Chuỗi có dạng
K1 K2 K3 "0" CODE.Ví dụ chuỗi phản hồi: nếu sau lệnh K1=1, K2=0, K3=1 vàcode="0", chuỗi gửi lên là"101" + "0" + "0" = "10100". Mã CODE cuối bằng 0 chính là tín hiệu "board đã nhận lệnh xong". -
Đưa trạng thái relay thật vào chuỗi POST lên cloud
Bước cuối: quay lại
POSTDuLieuVeCloudDeHienThiTrenAPP()(bài 02, 07). Thay ba giá trị đặt cứng"0"củas00/s01/s02bằng trạng thái thật của ba relay. Từ nay app hiển thị đúng relay nào đang bật, relay nào đang tắt.Kiểm tra trên app: mở app AIoTSemi, bấm nút K1 và K3 sang ON — chỉ sau khoảnh khắc, relay dưới board "tách" đóng lại và thẻ thiết bị trên app hiện đúng K1, K3 đang bật, kèm nhiệt độ và độ ẩm sống. Đến đây board đã điều khiển được hai chiều trọn vẹn giữa app ↔ cloud ↔ thiết bị.Bước tiếp theo tự nhiên: thêm chế độ AUTO (mã CODE khác) để board tự đóng/ngắt relay theo nhiệt độ, độ ẩm hoặc theo lịch giờ — chỉ cần thêm mộtcasetrongThucThiTacVuTheoCODEvà một hàmCheDoAUTOtrong class Relay.