สร้างระบบสมาร์ทล็อค (Smart Lock) ด้วย ESP32 พร้อมควบคุมผ่าน Bluetooth และแอปพลิเคชัน Android
สวัสดีชาว Maker ทุกคนครับ! ระบบล็อกรหัสผ่านอิเล็กทรอนิกส์ (Electronic Codelock) หรือที่เราเรียกกันว่า Smart Lock เป็นสิ่งที่เข้ามาแทนที่กุญแจแบบเดิมๆ และกลายเป็นมาตรฐานของออฟฟิศ คอนโด และบ้านอัจฉริยะในปัจจุบันไปแล้ว [cite: 1]
ในบทความนี้ เราจะพาคุณขยับสเกลการทำโปรเจกต์ จากของเล่นธรรมดาๆ ให้กลายเป็น "โปรโตไทป์ Smart Lock ระดับโปร" ที่มีความแอดวานซ์ทั้งในฝั่งของฮาร์ดแวร์ ซอฟต์แวร์ และการจัดการพลังงานที่ดีกว่ากลอนประตูทั่วไปในตลาดครับ [cite: 1] โปรเจกต์นี้จะใช้ชิ้นส่วนมาตรฐานที่หาซื้อได้ทั่วไป นำมาประยุกต์เข้ากับกลไกล็อกของจริง เพื่อให้ได้การทดสอบที่สมจริงที่สุด [cite: 1]
ฮาร์ดแวร์ที่ต้องใช้ (Hardware Components) 🛠️
- บอร์ดพัฒนา ESP32 Development Board [cite: 1]
- เซนเซอร์วัดกระแสไฟ ACS712 (SparkFun Low Current Sensor Breakout) [cite: 1]
- ไมโครเซอร์โวมอเตอร์ SG90 [cite: 1]
- เซนเซอร์แม่เหล็ก Hall Effect Sensor [cite: 1]
- ไอซีเรกูเลเตอร์ Linear Regulator (7805) [cite: 1]
- ทรานซิสเตอร์ Power MOSFET N-Channel [cite: 1]
- แบตเตอรี่ลิเธียมไอออน (Lithium Ion) ชาร์จได้ 2 ก้อน [cite: 1]
💡 Maker's Tip: การเลือกใช้อุปกรณ์ฮาร์ดแวร์ที่แม่นยำอย่างเซนเซอร์วัดกระแสและมอเตอร์ จะช่วยให้ระบบ Smart Lock ของคุณทำงานได้อย่างเสถียร ไม่เกิดอาการมอเตอร์ค้างจนไหม้ครับ!
หากเพื่อนๆ คนไหนกำลังมองหา บอร์ด ESP32, เซนเซอร์ ACS712, เซนเซอร์ Hall Effect หรือเซอร์โวมอเตอร์ เพื่อเอาไปทำโปรเจกต์เจ๋งๆ แบบนี้ แวะมาช้อปปิ้งของแท้พร้อมส่งได้ที่ Globalbyte เลยครับ! ของครบ จบในที่เดียว
แนวคิดและการออกแบบกลไก (Mechanical Design) ⚙️
ในโปรเจกต์นี้ ผู้พัฒนาได้นำ Micro-servo SG90 มาดัดแปลงให้หมุนได้อย่างอิสระต่อเนื่อง (Continuous rotation) เพื่อใช้ขับเคลื่อนตัวล็อก [cite: 1] ข้อดีของวิธีนี้คือมันครอบจักรวาลครับ ถ้าในผลิตภัณฑ์จริงคุณอยากเปลี่ยนไปใช้เซอร์โวเฟืองเหล็กตัวใหญ่ หรือมอเตอร์แบบ Brushless ก็แทบไม่ต้องแก้โค้ดเลย [cite: 1]
นอกจากนี้ยังมีการทำ "ตลับลูกปืน (Bearing)" แบบพิเศษขึ้นมา เพื่อให้มอเตอร์สามารถเลื่อนกุญแจไปซ้าย-ขวาได้จำลองสภาพแวดล้อมที่สมจริงที่สุด [cite: 1] ฮาร์ดแวร์ทั้งหมดนี้ หากเปลี่ยนไปใช้อุปกรณ์แบบ SMD จะสามารถย่อส่วนลงแผ่น PCB เล็กๆ ได้สบายเลยครับ [cite: 1]
เจาะลึกซอฟต์แวร์และการตั้งค่าตัวแปร (Software Variables) 💻
โค้ดในโปรเจกต์นี้ถูกออกแบบมาอย่างชาญฉลาด ให้สามารถปรับแต่งการทำงานของกลอนได้ตั้งแต่ต้นไฟล์ โดยไม่ต้องไปไล่หาในโค้ดบรรทัดลึกๆ ครับ [cite: 1] มาดูตัวแปรสำคัญกัน:
-
OFFSET: ค่าที่ให้เซอร์โวหยุดหมุนเมื่อไม่มีคำสั่ง (ขึ้นอยู่กับตัวต้านทานในเซอร์โวที่ถูกดัดแปลง) [cite: 1]
-
S_U และ S_L: ความเร็วในการหมุนตอนปลดล็อก (Unlocking) และตอนล็อก (Locking) [cite: 1]
-
IGNORE_TIME: เวลาที่จะสั่งให้ปิดเซนเซอร์ Hall ไว้ชั่วคราวตอนเริ่มสั่งงานมอเตอร์ (ถ้าตั้งเป็น 0 มอเตอร์จะไม่ขยับเลย) [cite: 1]
-
SOFT_START_TIME: เวลาหน่วงเพื่อให้มอเตอร์เริ่มขยับ ช่วยป้องกันไม่ให้เซนเซอร์วัดกระแสไฟอ่านค่าไฟกระชาก (Current peaks) ตอนมอเตอร์ออกตัว ไม่งั้นระบบจะนึกว่ามอเตอร์ติดขัด [cite: 1]
-
BRAKE_PULSE: หน่วยไมโครวินาที ใช้ปรับจูนละเอียดให้แม่เหล็กหยุดตรงตำแหน่งเซนเซอร์ HOME พอดีเป๊ะ! [cite: 1]
-
CURRENT_LIMIT: หากกระแสไฟพุ่งเกินค่ามิลลิแอมป์นี้ ระบบจะถือว่า "มอเตอร์ขัดข้อง (Stalled)" และสั่งตัดไฟทันที เพื่อป้องกันมอเตอร์ไหม้ [cite: 1]
-
STALL_SAMPLES: ต้องอ่านค่ากระแสไฟเกินกี่ครั้งติดกัน ถึงจะสั่งตัดไฟ (กันสัญญาณหลอก) [cite: 1]
-
vZero: ใช้เพื่อสอบเทียบ (Calibration) แรงดันแบตเตอรี่ให้แสดงผลถูกต้อง [cite: 1]
การควบคุมด้วยแอป Android ผ่าน BLE 📱
ด้านการควบคุมและกรอกรหัสผ่าน ผู้พัฒนาได้เขียนแอปพลิเคชัน Android ขึ้นมาเองครับ แม้จะเป็นการเขียนแอปครั้งแรกแต่มันก็ตอบโจทย์การทำงานเต็มรูปแบบ: [cite: 1]
- เมื่อเปิดแอป จะมีหน้าจอให้กรอกรหัส PIN Code เป็นด่านแรกเพื่อความปลอดภัย [cite: 1]
- เมื่อใส่รหัสถูก แอปจะเชื่อมต่อกับ Smart Lock ผ่าน BLE (Bluetooth Low Energy) [cite: 1]
-
Main Menu: แสดงสถานะว่าล็อกอยู่ (Locked) หรือปลดล็อก (Unlocked) พร้อมโชว์ระดับแบตเตอรี่ด้วยแถบสีต่างๆ ตรงกลางมีปุ่มกลมใหญ่ๆ ไว้สั่งเปิด-ปิดประตู ใช้งานง่ายสุดๆ [cite: 1]
-
Settings: หน้าตั้งค่าสามารถเลือกได้ว่า จะให้กุญแจบิด 1 รอบ หรือ 2 รอบ [cite: 1] แถมยังมีปุ่ม L และ R สำหรับบังคับมอเตอร์แบบแมนนวล เผื่อในกรณีกุญแจติดขัด ปุ่มนี้จะจ่ายไฟสูงสุดและข้ามเซนเซอร์กระแสไฟไปเลยครับ [cite: 1]
ทดสอบการกินไฟ และการจัดการพลังงาน (Power Consumption) 🔋
สิ่งสำคัญที่สุดของ Smart Lock คือ "มันต้องประหยัดแบตเตอรี่" [cite: 1] จากการนำแอมป์มิเตอร์มาวัดตอนเครื่องสแตนด์บาย (Idle) พบว่ากินกระแสไฟอยู่ที่ประมาณ 30 mA [cite: 1] ซึ่งสำหรับกลอนประตูถือว่าเยอะครับ!
โค้ดนี้ใช้โหมด Light Sleep แทน Deep Sleep เพราะมันช่วยให้เชื่อมต่อ BLE ได้แทบจะทันที (ถ้าใช้ Deep Sleep จะใช้เวลาตื่นขึ้นมาเชื่อมต่อบลูทูธนานเป็นสิบวินาที ซึ่งไม่ทันใจคนรอเปิดประตูแน่ๆ) [cite: 1]
ตัวการที่สูบแบตที่สุดตอนนี้คือ Linear Regulator (7805) ที่ทำหน้าที่ลดไฟจาก 8V ให้เหลือ 3.3V สำหรับ ESP32 [cite: 1] ถ้าเราเปลี่ยนไปใช้ Switching Mode Regulator การกินกระแสจะลดลงเหลือแค่ 3 ถึง 5 mA เท่านั้น ซึ่งเป็นระดับที่ต่ำที่สุดเท่าที่ฮาร์ดแวร์ชุดนี้จะทำได้ครับ! [cite: 1]
💡 ข้อเท็จจริงเกี่ยวกับแบตเตอรี่:
แม้จะปรับแต่งสุดๆ แล้ว แบตเตอรี่ของโปรเจกต์นี้ก็อาจอยู่ได้แค่ประมาณ 1 สัปดาห์ครับ [cite: 1] ความลับที่ทำให้กลอนประตูแบรนด์ดังๆ อยู่ได้นาน 3-6 เดือน คือพวกเขาใช้ชิปเฉพาะทางระดับอุตสาหกรรม (อย่างเช่นของ Nordic Semiconductors) ที่กินไฟน้อยกว่า ESP32 หลายพันเท่าเวลามันสแตนด์บาย [cite: 1] แต่นี่คือโปรโตไทป์ที่สมบูรณ์แบบสำหรับการเรียนรู้ตรรกะการทำงาน (Programming logic) ก่อนจะก้าวไปสู่ชิประดับอุตสาหกรรมครับ [cite: 1]
วิดีโอสาธิตการทำงาน (Demo Video) 🎬
ตัวอย่างระบบกลอนประตูดิจิทัล (Codelock) เชิงพาณิชย์
ชุดฮาร์ดแวร์ต้นแบบ (Prototype) ที่ถูกประกอบลงบนแผ่นพลาสติก
โมดูลเซนเซอร์วัดกระแสไฟ ACS712 สำหรับตรวจจับมอเตอร์ขัดข้อง
เซนเซอร์แม่เหล็ก Hall Effect ใช้กำหนดจุดหยุดของกุญแจ
การตั้งค่าตัวแปรในโค้ดที่สามารถปรับเปลี่ยนได้ง่าย
หน้าจอใส่รหัสผ่าน (PIN) เพื่อเข้าสู่แอปพลิเคชัน
หน้าจอหลักสำหรับสั่งปลดล็อก/ล็อก พร้อมแสดงสถานะแบตเตอรี่
หน้าจอ Settings สำหรับปรับรอบหมุนและสั่งงานแมนนวล
การวัดอัตราการกินกระแสไฟ (Power Consumption) ในโหมดสแตนด์บาย
ตัวจ่ายไฟ Linear Regulator ซึ่งเป็นตัวกินพลังงานหลักในโปรโตไทป์นี้
ผังวงจร (Schematics) ของระบบ Smart Lock
โค้ดฉบับสมบูรณ์ (Complete ESP32 Code) 🧑💻
คุณสามารถคัดลอกโค้ด C++ นี้ไปใช้ใน Arduino IDE เพื่อแฟลชลงบอร์ด ESP32 ได้เลยครับ [cite: 1]
// by mircemk March, 2026
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <ESP32Servo.h>
#include "esp_pm.h"
#include "esp_wifi.h"
// --- КОНФИГУРАЦИЈА ПИНОВИ (PIN CONFIGURATION) ---
const int PIN_SERVO = 18;
const int PIN_HALL = 19;
const int PIN_CURRENT = 4;
const int PIN_BATTERY = 35;
const int PIN_MOSFET = 2; // Control power for ACS712 and Servo
// --- ТАЈМЕРИ (TIMERS) ---
unsigned long lastWatchdogTick = 0;
const unsigned long WATCHDOG_TIMEOUT = 300;
unsigned long lastBatteryReport = 0;
const unsigned long BATTERY_REPORT_INTERVAL = 5000;
// --- ПАРАМЕТРИ (PARAMETERS) ---
RTC_DATA_ATTR int turnsNeeded = 1;
int OFFSET = 5;
const int S_U = 4;
const int S_L = -4;
const unsigned long IGNORE_TIME = 600;
const unsigned long SOFT_START_TIME = 400;
const unsigned long BRAKE_PULSE = 25000;
const int CURRENT_LIMIT = 920;
const int STALL_SAMPLES = 3;
float vZero = 2.5;
#define SERVICE_UUID "12345678-1234-1234-1234-1234567890ab"
#define COMMAND_CHAR_UUID "abcd1234-5678-1234-5678-1234567890ab"
#define STATUS_CHAR_UUID "dcba4321-8765-4321-8765-1234567890ab"
Servo myServo;
BLECharacteristic *pStatusCharacteristic;
bool deviceConnected = false;
bool lastConnectionState = false;
volatile bool magnetHit = false;
bool hallEnabled = false;
bool isManualMode = false;
unsigned long moveStartTime = 0;
int lastDir = 0;
int stallCounter = 0;
int magnetCount = 0;
String lockStatus = "LOCKED";
String lastKnownValidStatus = "LOCKED";
// --- ПОМОШНИ ФУНКЦИИ (HELPER FUNCTIONS) ---
void IRAM_ATTR onHall() {
if (hallEnabled && !isManualMode) magnetHit = true;
}
void updateStatus(String newStatus) {
lockStatus = newStatus;
if (newStatus == "LOCKED" || newStatus == "UNLOCKED") lastKnownValidStatus = newStatus;
pStatusCharacteristic->setValue(lockStatus.c_str());
pStatusCharacteristic->notify();
}
void stopAction(String finalStatus) {
int tempDir = lastDir;
lastDir = 0; moveStartTime = 0; hallEnabled = false; stallCounter = 0; magnetCount = 0;
if (tempDir == 1) myServo.write(S_L + 90 + OFFSET);
else if (tempDir == -1) myServo.write(S_U + 90 + OFFSET);
if (tempDir != 0) ets_delay_us(BRAKE_PULSE);
myServo.write(90 + OFFSET);
// Power OFF peripheral (Energy saving)
digitalWrite(PIN_MOSFET, LOW);
updateStatus(finalStatus);
}
float readBatteryVoltage() {
long sum = 0;
const int samples = 40;
for (int i = 0; i < samples; i++) {
sum += analogRead(PIN_BATTERY);
delayMicroseconds(400);
}
float adcRaw = (float)sum / (float)samples;
float vAdc = (adcRaw / 4095.0f) * 3.3f;
float vBat = vAdc * 3.2f * 1.1f;
return vBat;
}
void reportBatteryVoltage() {
float vb = readBatteryVoltage();
if (deviceConnected) {
String msg = "BAT:" + String(vb, 2) + "V";
pStatusCharacteristic->setValue(msg.c_str());
pStatusCharacteristic->notify();
}
}
float readCurrent() {
long sum = 0;
for (int i = 0; i < 15; i++) sum += analogRead(PIN_CURRENT);
float voltage = ((float)sum / 15.0f * 3.3f) / 4095.0f;
float current = (voltage - vZero) / 0.185f;
return abs(current * 1000.0f);
}
void handleCommand(char cmd) {
// Before any action, power ON motor and sensor
if (cmd == 'U' || cmd == 'L' || cmd == '[' || cmd == ']' || cmd == 'M') {
digitalWrite(PIN_MOSFET, HIGH);
delay(20); // Pause for voltage stabilization
}
if (isManualMode) {
if (cmd == '[' || cmd == ']') {
lastWatchdogTick = millis();
if (cmd == '[') { lastDir = 1; myServo.write(S_U + 90 + OFFSET); }
else { lastDir = -1; myServo.write(S_L + 90 + OFFSET); }
return;
}
if (cmd == '1') { turnsNeeded = 1; updateStatus("SET_1_TURN"); }
else if (cmd == '2') { turnsNeeded = 2; updateStatus("SET_2_TURNS"); }
else if (cmd == 'S') { stopAction("MAN_STOP"); }
if (cmd == '1' || cmd == '2' || cmd == 'S') { delay(500); updateStatus(lastKnownValidStatus); }
}
if (cmd == 'M') {
isManualMode = !isManualMode;
stopAction(isManualMode ? "MANUAL_ON" : "MANUAL_OFF");
if (!isManualMode) { delay(500); updateStatus(lastKnownValidStatus); }
}
else if (!isManualMode) {
if (cmd == 'U' || cmd == 'L') {
magnetHit = false; hallEnabled = false; stallCounter = 0; magnetCount = 0;
moveStartTime = millis();
if (cmd == 'U') { lastDir = 1; myServo.write(S_U + 90 + OFFSET); updateStatus("UNLOCKING"); }
else { lastDir = -1; myServo.write(S_L + 90 + OFFSET); updateStatus("LOCKING"); }
}
else if (cmd == 'S') { stopAction(lockStatus); }
}
}
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) { deviceConnected = true; }
void onDisconnect(BLEServer* pServer) { deviceConnected = false; BLEDevice::startAdvertising(); }
};
class CommandCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string rxValue = pCharacteristic->getValue();
if (rxValue.length() > 0) handleCommand(rxValue[0]);
}
};
void setup() {
// 1. ENERGY OPTIMIZATION (Auto Light Sleep between BLE events)
esp_wifi_stop();
esp_pm_config_esp32_t pm_config;
pm_config.max_freq_mhz = 80;
pm_config.min_freq_mhz = 10;
pm_config.light_sleep_enable = true;
esp_pm_configure(&pm_config);
Serial.begin(115200);
// 2. MOSFET SETUP (Main switch)
pinMode(PIN_MOSFET, OUTPUT);
digitalWrite(PIN_MOSFET, LOW); // Start with peripherals OFF
// 3. HARDWARE
ESP32PWM::allocateTimer(0);
myServo.setPeriodHertz(50);
myServo.attach(PIN_SERVO, 500, 2400);
pinMode(PIN_HALL, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_HALL), onHall, FALLING);
pinMode(PIN_BATTERY, INPUT);
analogReadResolution(12);
// 4. BLE SETUP
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
BLEDevice::init("BLE_LOCK_TEST");
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID);
BLECharacteristic *pCmdChar = pService->createCharacteristic(COMMAND_CHAR_UUID, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_READ);
pCmdChar->setCallbacks(new CommandCallbacks());
pStatusCharacteristic = pService->createCharacteristic(STATUS_CHAR_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
pStatusCharacteristic->addDescriptor(new BLE2902());
pStatusCharacteristic->setValue(lockStatus.c_str());
pService->start();
BLEDevice::getAdvertising()->start();
myServo.write(90 + OFFSET);
Serial.println("System v1.7 Ready - Peripherals OFF");
}
void loop() {
if (Serial.available() > 0) handleCommand(Serial.read());
if (deviceConnected && !lastConnectionState) {
updateStatus(lockStatus);
}
lastConnectionState = deviceConnected;
if (millis() - lastBatteryReport >= BATTERY_REPORT_INTERVAL) {
lastBatteryReport = millis();
reportBatteryVoltage();
}
if (isManualMode && lastDir != 0) {
if (millis() - lastWatchdogTick > WATCHDOG_TIMEOUT) stopAction(lastKnownValidStatus);
}
if (lastDir != 0 && !isManualMode) {
if (millis() - moveStartTime > SOFT_START_TIME) {
float current = readCurrent();
if (current > CURRENT_LIMIT) {
stallCounter++;
if (stallCounter >= STALL_SAMPLES) stopAction("Z");
} else { if (stallCounter > 0) stallCounter--; }
}
if (magnetHit) {
magnetCount++;
if (magnetCount >= turnsNeeded) stopAction(lastDir == 1 ? "UNLOCKED" : "LOCKED");
else { magnetHit = false; hallEnabled = false; moveStartTime = millis(); }
}
if (!hallEnabled && moveStartTime != 0 && (millis() - moveStartTime > IGNORE_TIME)) hallEnabled = true;
}
delay(10);
}
*คำเตือน: เนื้อหานี้เป็นการสรุปและเรียบเรียงจากบทความเทคโนโลยีต้นฉบับภาษาอังกฤษ ข้อมูลฉบับภาษาไทยอาจมีความคลาดเคลื่อนบางประการจากการตีความหรือย่อเนื้อหา การดัดแปลงมอเตอร์และการต่อวงจรกับแบตเตอรี่ลิเธียมไอออนมีความเสี่ยงต่อการลัดวงจร ผู้สนใจควรศึกษาเรื่องความปลอดภัยทางไฟฟ้า สามารถตรวจสอบรายละเอียดเชิงเทคนิคและโค้ดต้นฉบับแบบเต็มได้ที่
เว็บไซต์ต้นฉบับ ก่อนลงมือทำโปรเจกต์