ทำไมของทอดจึงหอมอร่อยจัง ?

ถ้าหากเอาแบบวิทยาศาสตร์เลยก็ต้องพูดถึง Maillard Reaction  มันเป็นปฏิกิริยาเคมีที่เมื่อบรรดาสารประกอบไนโตรเจน กรดอะมิโนและน้ำตาลถูกความร้อนสูงจากการ “ทอด” (150 – 170 ℃) จัดเรียงโครงสร้างใหม่ และพวก volatile aromatic compounds จะกลายเป็นไอออกมาเยอะมาก …. ซึ่งก็คือ “กลิ่น” อันยั่วยวนเรานั่นเอง 😂😁

Maillard Reaction ยังทำให้เกิดรสชาติใหม่หลากหลาย + เพิ่มความเข้มข้นของรส “อูมามิ”  นี่เองทำให้ของทอดอร่อยถูกใจ  พวกของทอดทั้งหลาย ถึงแม้จะเป็นวัตถุดิบราคาถูก อย่างไส้กรอกแป้งไม้ละ 5 บาท10 บาทก็หอมทั้งนั้นเนื่องจาก Maillard Reaction

ปฏิกิริยาเคมี  Maillard Reaction ที่สร้างความหอมอร่อยที่ว่านี้ไม่จำเป็นต้องทอดอย่างเดียว นำไปอบก็เกิดได้ ขอเพียงแค่มีอุณหภูมิในช่วง 150 – 170 ℃ อาหารก็สามารถเกิดปฏิกิริยานี้ได้ นี่เองเป็นเหตุผลที่ว่าอาหารประเภทต้มหรือนึ่งไม่เคยมีความหอมอร่อย เพราะปฏิกิริยานี้ไม่เกิดครับ (อาหารประเภท “ต้ม” จะมีความหอมน่ากินด้วยเครื่องเทศแทนครับ แต่ไม่ได้หอมจากปฏิกิริยานี้มันเกิดจากสารประกอบที่ระเหยง่ายในบรรดาเครื่องเทศระเหยออกมาจากการต้ม)


ส่วนน้ำมันจะมีผลน้อยครับ หรือจะว่าความหอมอร่อยไม่ได้มาจากน้ำมันซักเท่าไหร่ แต่เกิดจากปฏิกิริยาเคมีนี้ทั้งสิ้น …. “น้ำมัน” จะเพิ่มความหอมอร่อยได้ชัดเจนคือใช้น้ำมันหมูทำกับข้าว อย่างเช่น ผัดกะเพราบ้าน ๆ นี่แหละ ถ้าใช้น้ำมันหมูแบบที่เจียวจากมันหมูมันจะมีความหอมแบบประหลาด เพิ่มความอร่อยทวีคูณ

ถ้าเป็นการทำอาหารในเชิงอุตสาหกรรมหรือร้านอาหาร/ภัตตาคารใหญ่ เขาจะมีสิ่งที่เรียกว่า Lard   มันคือผลิตภัณฑ์ไขมันสีขาวกึ่งแข็งที่ได้จากการแปรรูปเนื้อเยื่อไขมันของหมู

หากทำกินเองก็เจียวจากมันหมูนี่แหละที่สุดแล้ว


เห็นภาพไก่ทอด …. ขอพูดถึงไก่ KFC สูตรดั้งเดิมหน่อย  ก็คือที่ KFC เรียกกันว่า “สูตรผู้พัน

สูตรผู้พันเนี่ยผมกินมาตลอด 30 ปี ไม่เคยเปลี่ยนเลย ไม่เคยสั่งสูตรไก่กรอบฮอตสไปซี่อะไรทั้งนั้น กินแต่สูตรผู้พันอย่างเดียวเนื่องจากเครื่องเทศครบมาเต็ม …. ครั้งต่อไปหากท่านไปกิน KFC ลองสั่งสูตรผู้พันดูนะแล้วจะติดใจในความหอมเครื่องเทศ

ESP32 (EP.4) ESP32 กับ dashboard + แจ้งเตือนผ่าน Line

จะขอเสนอการนำ ESP32 ไปใช้งานกับ web dashboard บน server จริง และมี feature ที่สำคัญก็คือ Line messaging API (แจ้งเตือนผ่าน Line)

การใช้งานกับ web dashboard บน server จริงคือการประยุกต์ใช้งานแบบ IoT เต็มตัวครับ ภาพรวมของการใช้งานแบบนี้ก็คือ ESP32 จะรับ data จาก sensor ต่าง ๆ และต่อ GPIO ออกไปเพื่อควบคุมอุปกรณ์ / เครื่องใช้ไฟฟ้าในบ้าน  และในฝั่งปลายทาง PC / Notebook / มือถือของเรา) ก็จะเป็น web dashboard ซึ่งสามารถดูข้อมูลหรือสถานะของ sensor หรือระบบรักษาความปลอดภัยที่บ้านผ่าน web dashboard นอกจากดูก็สามารถกดปุ่มสั่งงานเครื่องใช้ไฟฟ้า/อุปกรณ์ใด ๆ ที่บ้านเราได้ นี่คือการใช้งานในลักษณะ IoT เต็มตัว

สิ่งที่ต้องมี 2 อย่าง
🛑 อย่างแรกคือเว็บไซต์ส่วนตัว
การใช้งาน IoT ได้แบบคล่องตัวมากที่สุดสิ่งที่ต้องมีเลยก็คือเว็บไซต์ส่วนตัวของเรา ซึ่งในปัจจุบันนี้การจะจดทะเบียน Domain name + เช่า web hosting บอกได้เลยว่าราคาต่อปีไม่ถึง 1,000 บาทก็มีครับ ถูกสุดที่ผมเคยเห็นประมาณ 850 บาทก็ได้แล้ว ดังนั้นจึงขอแนะนำอย่างยิ่งสำหรับท่านที่ต้องการเรียนรู้การทำ IoT project หรือจะฝึกฝีมือกับ ESP32 เพื่อใช้งาน IoT เต็มรูปแบบก็ควรสมัครเว็บไซต์ส่วนตัวไว้

อีกประเภทหนึ่งที่นิยมกันมากก็คือ “Cloud IoT” อย่างเช่น ThingsBoard , Adafruit IO , Blynk , Thinger.io , ThingSpeak  มันคือแพลตฟอร์มที่ใช้ได้ฟรีสามารถแสดง dashboard ได้และแทบไม่ต้องใช้ code เลย ลักษณะการสร้าง dashboard + เชื่อมข้อมูลกับ ESP32 ของเราจะเป็นการ “ลากแล้ววาง” แต่ผมคิดว่ามันใช้งานไม่ค่อยยืดหยุ่นสักเท่าไหร่ / ปรับแต่งเองได้ไม่เต็มที่และที่สำคัญก็คือใช้กับ Line messaging API ได้ยาก

🛑 อย่างที่สองคือ Line messaging API
Line messaging API ที่จะใช้งานในโปรเจคอย่างกระทู้นี้ ทุกคนก็สามารถสมัครได้ฟรีโดยใช้งานได้ 300 ข้อความต่อเดือนซึ่งก็ถือว่าเหลือเฟือในการใช้งานเพื่อทดลองและเรียนรู้ครับ …. การใช้ Line messaging API จะต้องสมัคร LINE Official Account ก่อน … เริ่มที่นี่ครับ
https://manager.line.biz/
ดูวิธีการสมัครที่นี่
(คลิปจากช่อง ครูอภิวัฒน์ “สอนสร้างสื่อ”
https://www.youtube.com/@KruApiwat



เมื่อสมัคร LINE Official Account เสร็จแล้วก็ไป enable messaging API …. และเมื่อทุกอย่างเรียบร้อยแล้วทั้ง 4 ภาพด้านล่างนี้คือข้อมูล 2 ส่วนที่จะต้องใช้ไปใส่ใน ESP32 code ครับ


ต่อไปคือสถาปัตยกรรมในระบบ IoT ทั่วไป (ซึ่ง project นี้ก็ใช้แบบนี้)

จากภาพบนนี้  ESP32 ที่บ้านหรือที่ทำงานเราก็ทำหน้าที่เป็นเพียง client เพื่อที่จะส่ง-รับข้อมูลเข้าระหว่าง web server  ในส่วนของ web server ก็จะเป็นที่เก็บ dashboard + database ซึ่ง database ไม่ต้องยุ่งยากเป็นแบบ SQL เลยครับ ใช้แบบ text file database (JSON) ก็ใช้งานได้ดีเพียงพอแล้วกับ sensor 2 – 3 ตัวและการเก็บข้อมูลประมาณ 1 สัปดาห์ต่อเนื่องกัน


ใน project ที่ผมจะสาธิตให้ดูในกระทู้นี้หน้าตาของ web dashboard ก็คือตามภาพล่างครับ  

ผมต่อ sensor ไว้ 2 ตัวก็คือ DHT22 กับ LDR เพื่อแสดงค่าอุณหภูมิและความสว่างของแสงแสดงเป็นกราฟทุก 1 นาทีและได้รวมระบบ Line messaging API ไว้ด้วย  โดยใน Code กำหนดให้ส่งข้อความแจ้งเตือนเมื่อมีการเปิดประตูและแสงสว่างมีค่าต่ำกว่าที่กำหนดไว้ครับ

นี่คือภาพของอุปกรณ์ที่ใช้มีเพียงเท่านี้ ก็คือมี sensor 2 ตัวเท่านั้น

และในภาพนี้สังเกตจุดสีแดงกับน้ำเงินกระพริบอยู่ก็คือเป็นสาย jumper 2 เส้นที่ผมใช้แทน switch จำลองการเปิดประตูด้านหน้าและด้านหลังส่งข้อมูลไปยังเว็บ server เพื่อส่งข้อความเข้า LINE ในมือถือเมื่อประตูถูกเปิดหรือเมื่อความสว่างของแสงน้อยกว่าที่กำหนดครับ


คลิบนี้แสดงถึงการแจ้งเตือนผ่าน Line

Code ทั้งหมด

/*
   ESP32-S3 N16R8 Data Logger & Monitor (Core 3.3.1)
   Hardware Connections:
   - DHT22 Data Pin: GPIO 5 (needs 10k pull-up resistor to 3.3V)
   - LDR/Voltage Sensor: GPIO 1 (Analog input ADC1_CH0)
   - Front Door Sensor: GPIO 7 (Logic input with internal pull-up)
   - Back Door Sensor: GPIO 8 (Logic input with internal pull-up)
*/

#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <DHT.h>

// Wi-Fi Credentials
const char* ssid     = "NT91124G";
const char* password = "911928TOT";

// API Endpoints & Credentials
const char* serverUrl = "https://www.navy007.com/temp32/data_receiver.php";
const char* lineApiUrl = "https://api.line.me/v2/bot/message/push";
const char* lineToken  = " Line Token ของท่าน "; 
const char* lineUserId = " User ID ของท่าน ";

// Pins
#define DHTPIN 5
#define DHTTYPE DHT22
#define VOLT_PIN 1
#define DOOR_FRONT_PIN 7
#define DOOR_BACK_PIN 8

DHT dht(DHTPIN, DHTTYPE);

// Timers (Non-blocking)
unsigned long lastDHTSend = 0;
unsigned long lastSensorRead = 0;
const unsigned long dhtInterval = 60000;   // 1 minute
const unsigned long sensorInterval = 5000; // 5 seconds

// State tracking for LINE Spam Prevention
enum LightState { DARK, PARTLY_CLOUD, STRONG_SUN };
LightState currentLightState = STRONG_SUN; 
bool isFirstLightRead = true;

int lastFrontDoorState = -1; 
int lastBackDoorState = -1;

// Float definitions for tracking current values
float currentTemperature = 0.0;
float currentHumidity = 0.0;
float currentVoltGPIO1 = 0.0;

void setup() {
  Serial.begin(115200);
  
  // Setup Sensor Pins
  dht.begin();
  pinMode(VOLT_PIN, INPUT); // ADC1_CH0 (GPIO 1 is safe for ADC)
  pinMode(DOOR_FRONT_PIN, INPUT_PULLUP);
  pinMode(DOOR_BACK_PIN, INPUT_PULLUP);
  
  // Setup Wi-Fi
  WiFi.begin(ssid, password);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConnected to Wi-Fi Network!");
}

void loop() {
  unsigned long currentMillis = millis();

  // Task 1: Pull DHT22 Data every 1 minute
  if (currentMillis - lastDHTSend >= dhtInterval) {
    lastDHTSend = currentMillis;
    readDHT22();
    sendDataToServer();
  }

  // Task 2 & 3: Monitor GPIO 1, 7, and 8 every 5 seconds
  if (currentMillis - lastSensorRead >= sensorInterval) {
    lastSensorRead = currentMillis;
    monitorPins();
  }
}

void readDHT22() {
  float temp = dht.readTemperature();
  float hum = dht.readHumidity();
  if (!isnan(temp) && !isnan(hum)) {
    currentTemperature = temp;
    currentHumidity = hum;
    Serial.printf("DHT22 - Temp: %.2f C, Hum: %.2f%%\n", temp, hum);
  } else {
    Serial.println("Error reading from DHT22");
  }
}

void monitorPins() {
  // --- GPIO 1 Voltage Monitoring ---
  // ESP32-S3 ADC has 12-bit resolution (0-4095) spanning ~3.1V depending on calibration
  int rawADC = analogRead(VOLT_PIN);
  currentVoltGPIO1 = (rawADC / 4095.0) * 3.3; // Estimated voltage scale

  LightState computedState;
  if (currentVoltGPIO1 < 2.0) {
    computedState = DARK;
  } else if (currentVoltGPIO1 >= 2.0 && currentVoltGPIO1 <= 2.35) {
    computedState = PARTLY_CLOUD;
  } else {
    computedState = STRONG_SUN;
  }

  // Handle state change for light sensor to prevent spamming
  if (computedState != currentLightState || isFirstLightRead) {
    currentLightState = computedState;
    isFirstLightRead = false;
    
    if (currentLightState == DARK) {
      sendLineMessage("ระดับแสงต่ำเกินไป");
    }
  }

  // --- GPIO 7 (Front Door) & GPIO 8 (Back Door) Monitoring ---
  int frontDoor = digitalRead(DOOR_FRONT_PIN);
  int backDoor = digitalRead(DOOR_BACK_PIN);

  // Spam prevention check for front door
  if (frontDoor != lastFrontDoorState) {
    lastFrontDoorState = frontDoor;
    if (frontDoor == HIGH) { // Logic 1 = Open
      sendLineMessage("ประตูหน้าเปิด");
    }
  }

  // Spam prevention check for back door
  if (backDoor != lastBackDoorState) {
    lastBackDoorState = backDoor;
    if (backDoor == HIGH) { // Logic 1 = Open
      sendLineMessage("ประตูหลังเปิด");
    }
  }

  Serial.printf("GPIO1: %.2f V, Front: %d, Back: %d\n", currentVoltGPIO1, frontDoor, backDoor);
}

// Function to Send Official LINE Push Notification
void sendLineMessage(String message) {
  if (WiFi.status() != WL_CONNECTED) return;

  WiFiClientSecure client;
  client.setInsecure(); // Required for share hosting or API calls where cert pinning isn't hardcoded
  HTTPClient http;

  http.begin(client, lineApiUrl);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("Authorization", "Bearer " + String(lineToken));

  // Construct official LINE Push Message JSON
  String jsonPayload = "{\"to\":\"" + String(lineUserId) + "\",\"messages\":[{\"type\":\"text\",\"text\":\"" + message + "\"}]}";

  int httpResponseCode = http.POST(jsonPayload);
  if (httpResponseCode > 0) {
    Serial.println("LINE Push Sent successfully: " + String(httpResponseCode));
  } else {
    Serial.println(String("Error sending LINE Push: ") + http.errorToString(httpResponseCode).c_str());
  }
  http.end();
}

// Push latest telemetry to the Apache Share Hosting Server
void sendDataToServer() {
  if (WiFi.status() != WL_CONNECTED) return;

  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;

  http.begin(client, serverUrl);
  http.addHeader("Content-Type", "application/x-www-form-urlencoded");

  // Format POST payload
  String postData = "temp=" + String(currentTemperature, 2) +
                    "&volt=" + String(currentVoltGPIO1, 2) +
                    "&front_door=" + String(lastFrontDoorState) +
                    "&back_door=" + String(lastBackDoorState);

  int httpResponseCode = http.POST(postData);
  if (httpResponseCode > 0) {
    Serial.println("Telemetry synced: " + String(httpResponseCode));
  } else {
    Serial.println(String("Error syncing telemetry: ") + http.errorToString(httpResponseCode).c_str());
  }
  http.end();
}

ESP32 (EP.3) ทำงานกับ sensor ต่าง ๆ

ตั้งแต่ยุคของ Arduino ก็มีพวก sensor ชนิดต่าง ๆ เกือบ 100 ชนิดก็ว่าได้  มีทั้ง sensor แบบ analog , digital  ซึ่ง sensor เหล่านั้นก็นำมาใช้กับ ESP32 ได้อย่างสมบูรณ์  สิ่งเดียวที่ต้องดูก็คือ sensor เหล่านั้นใช้ไฟเลี้ยงเท่าใด 5 โวลต์หรือ 3.3 โวลต์  หากเป็น sensor ในยุคของ Arduino ก็จะเป็นไฟเลี้ยง 5 โวลต์ เมื่อเชื่อมต่อกับ ESP32 (ซึ่งรับ input เข้า GPIO 3.3V)  ก็จะต้องมีอุปกรณ์ลดแรงดันลงจากเกือบ 5V เป็น 3.3V (ใช้ voltage divider ง่ายสุด)

ภาพตัวอย่าง sensors ที่ใช้กับ ESP32 , Arduino

sensor ทั้งหลายเหล่านี้ถูกผลิตออกมาครอบคลุมการใช้งาน IoT ทั้งหมด  ที่ใช้กันมากก็เช่น sensor อุณหภูมิ ความชื้น ความสว่าง ระดับเสียง วัดระยะทาง  นอกจากนี้ยังมี sensor พื้นฐานที่ใช้ในอิเล็กทรอนิกส์ยุคเก่ามาประยุกต์เชื่อมต่อกับ ESP32 ก็ได้อย่างเช่น LDR , Thermistor , ตัวรับสัญญาณ infrared , IR LED


🛑 ตัวแรกก็คือ DHT22  มันเป็น sensor อุณหภูมิกับความชื้นแบบ digital ก็คือมีขา output เพียงขาเดียวเท่านั้นและต่อกับ GPIO ของ ESP32 โดยตรงได้เลยครับเพราะใช้ไฟเลี้ยง 3.3V  ข้างใน sensor ตัวนี้จะมี sensor รับความชื้นและ Thermister เพื่อรับอุณหภูมิและมีไมโครคอนโทรลเลอร์ 8 bit ขนาดจิ๋วด้านในเพื่อแปลงสัญญาณ analog จาก sensor ความชื้นและ thermister แปลงไปเป็นสัญญาณ serial  และเนื่องจาก DHT22 เป็นแบบ digital output ดังนั้นจึงต้องทำงานกับ library ของมันเอง


🛑 ตัวต่อไปคือ HY-SRF05 Ultrasonic Module  เป็น sensor ที่ใช้คลื่นเสียงความถี่สูงระดับ ultrasonic 40 KHz โดยส่งคลื่นออกไปและคำนวณเวลาที่คลื่นสะท้อนกลับมา สามารถวัดระยะวัตถุได้แม่นยำตั้งแต่ 2 ซม. – 4 เมตร  ตัวนี้เป็น sensor ที่สมบูรณ์ในตัวเอง ก็คือมี IC ทำหน้าที่กำเนิด ultrasonic และเป็นภาครับ echo ของคลื่น ultrasonic กลับมาแปลงมาเป็นค่า timing ได้เลย

Timing diagram ของ HY-SRF05 Ultrasonic Module ตามภาพนี้ครับ

ถึงแม้ Timing diagram จะดูซับซ้อนยุ่งยาก  แต่เวลาให้ AI เขียน code เราไม่ต้องบอกรายละเอียดพวกนี้เลยครับ เพราะ AI ถูก train ข้อมูลพวกนี้มาหมดแล้ว มันสามารถออก code ที่สอดคล้องกับ timing diagram นี้แบบเป๊ะเลย  ใน prompt เราแค่ระบุให้ชัดว่าขา TRIG , Echo ต่อกับ GPIO ใดของ ESP32

ตัวอย่าง project วัดระยะทาง + LDR วัดความสว่างแสดงค่าแสงเป็น 0 – 100%

ESP32 code ของ project นี้

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <DHT.h>

// OLED Configuration
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDRESS 0x3C  // Common I2C address for these displays
#define I2C_SDA 40
#define I2C_SCL 42
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// DHT22 Configuration
#define DHTPIN 5
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

// LDR Configuration
#define LDR_PIN 1

// HY-SRF05 Configuration
#define TRIG_PIN 7
#define ECHO_PIN 8

// Timing Variables (Non-blocking using millis)
unsigned long lastDHTRead = 0;
unsigned long lastLDRRead = 0;
unsigned long lastSonicRead = 0;

const long dhtInterval = 5000;   // 5 seconds
const long ldrInterval = 300;    // 300 ms
const long sonicInterval = 1000; // 1 second

// Global variables to hold last read values
float currentTemp = 0.0;
float lightPercent = 0.0;
float currentRange = 0.0;

void setup() {
  Serial.begin(115200);

  // Initialize Custom I2C pins for ESP32-S3
  Wire.begin(I2C_SDA, I2C_SCL);

  // Initialize OLED
  if (!display.begin(OLED_ADDRESS, true)) {
    Serial.println(F("SH1106 OLED initialization failed!"));
    for (;;);
  }
  
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  display.display();

  // Initialize DHT
  dht.begin();

  // Initialize Ultrasonic Pins
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);

  // Configure ADC attenuation if needed for 0-3V range
  // By default, ESP32 Arduino Core 3.x configures ADC to 11dB (~0V - 3.3V)
  analogSetPinAttenuation(LDR_PIN, ADC_11db);
}

void loop() {
  unsigned long currentMillis = millis();
  bool needUpdateDisplay = false;

  // 1. Read DHT22 every 5 seconds
  if (currentMillis - lastDHTRead >= dhtInterval) {
    lastDHTRead = currentMillis;
    float t = dht.readTemperature();
    if (!isnan(t)) {
      currentTemp = t;
      needUpdateDisplay = true;
    }
  }

  // 2 & 3. Read LDR every 300 ms and map voltage
  if (currentMillis - lastLDRRead >= ldrInterval) {
    lastLDRRead = currentMillis;
    
    // Read raw ADC (ESP32-S3 has 12-bit ADC: 0 - 4095)
    int rawADC = analogRead(LDR_PIN);
    
    // Convert to Voltage (assuming 3.3V VREF)
    float voltage = (rawADC / 4095.0) * 3.3;
    
    // Map 1.0V - 3.0V to 0% - 100%
    if (voltage < 1.0) {
      lightPercent = 0.0;
    } else if (voltage > 3.0) {
      lightPercent = 100.0;
    } else {
      // Linear interpolation formula: (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
      lightPercent = (voltage - 1.0) * (100.0 - 0.0) / (3.0 - 1.0) + 0.0;
    }
    needUpdateDisplay = true;
  }

  // 4. Read HY-SRF05 Ultrasonic every 1 second
  if (currentMillis - lastSonicRead >= sonicInterval) {
    lastSonicRead = currentMillis;
    
    // Trigger the sensor
    digitalWrite(TRIG_PIN, LOW);
    delayMicroseconds(2);
    digitalWrite(TRIG_PIN, HIGH);
    delayMicroseconds(10);
    digitalWrite(TRIG_PIN, LOW);
    
    // Read echo duration (travel time in microseconds)
    long duration = pulseIn(ECHO_PIN, HIGH, 30000); // 30ms timeout (~5 meters max)
    
    // Calculate distance in cm (Speed of sound is ~343 m/s or 0.0343 cm/us)
    if (duration > 0) {
      currentRange = (duration * 0.0343) / 2.0;
    } else {
      currentRange = -1.0; // Out of range / timeout indicator
    }
    needUpdateDisplay = true;
  }

  // Update OLED if any data has changed
  if (needUpdateDisplay) {
    updateOLED();
  }
}

void updateOLED() {
  display.clearDisplay();
  
  // Line 1: Temperature
  display.setCursor(0, 4);
  display.print("Temp: ");
  display.print(currentTemp, 1); // 1 decimal place
  display.print(" C");
  
  // Line 2: Light Level
  display.setCursor(0, 24);
  display.print("Light level = ");
  display.print(lightPercent, 1); // 1 decimal place
  display.print(" %");
  
  // Line 3: Range
  display.setCursor(0, 44);
  if (currentRange >= 0) {
    display.print("Range = ");
    display.print(currentRange, 1); // 1 decimal place
    display.print(" cm");
  } else {
    display.print("Range = Out of Bound");
  }
  
  display.display();
}

ต่อไปเป็น sensor ความสว่างของแสงก็คือ LDR (Light Dependent Resistor)  ตัว LDR นี้มีมาตั้งแต่ยุคก่อนไมโครคอนโทรลเลอร์อีกครับ มันเป็นตัวความต้านทานที่เปลี่ยนค่าได้ตามความเข้มของแสง  รุ่นที่นิยมใช้ก็จะมีหน้าตาแบบในภาพนี้ตัวเล็กเท่าหัวเข็มหมุดเท่านั้น  เมื่อแสงจ้ามากความต้านทานจะลดลงเหลือประมาณ 1 kΩ และเมื่อมืดเกือบสนิทความต้านทานจะเพิ่มขึ้นไปสูงถึงระดับ 100 -200  kΩ ครับ

ความรู้พื้นฐานทางด้านอิเล็กทรอนิกส์ที่ต้องใช้กับ LDR ก็คือวงจรแบ่งแรงดัน (Voltage divider) ก็คือเราจะต้องต่อ LDR กับตัวความต้านทาน 1 ตัวตามวงจรด้านล่างนี้  ส่วน R จะใช้ค่าเท่าไหร่ก็ขึ้นอยู่กับว่าระดับแสงที่เราต้องการวัดหรือตรวจจับมีความสอดคล้องกับความต้านทานของ LDR ที่เปลี่ยนไปมากน้อยเท่าใด  ซึ่งค่าความเข้มแสงก็สามารถแสดงผลได้หลายอย่างกับขึ้นอยู่กับ code ที่เราจะเขียนครับ  อย่างเช่นความสว่างระดับ 0 ถึง 100% หรืออาจจะแปลงเทียบเป็นความสว่างหน่วย Lux ก็ได้

ภาพวงจร voltage divider + LDR + NTC กับการต่อเข้า GPIO ของ ESP32


กรณีของ LDR , Thermistor (NTC) ผมจะขออธิบายลงลึกในเรื่องของวิศวกรรมและคณิตศาสตร์หน่อยนึง เนื่องจากผมตั้งใจจะเขียนบทความให้ลงลึกในทฤษฎีเพื่อที่จะสามารถออกแบบ project เองได้

การแปลงความสว่างไปเป็นหน่วย Lux ด้วย LDR
ขอเริ่มที่กราฟความต้านทานของ LDR ก่อน

จากภาพบน  ค่าความต้านทาน LDR เปลี่ยนแปลงไม่เป็นเชิงเส้นกับความสว่างของแสง (A ในภาพ) ดังนั้นจากการเปลี่ยนแปลงที่ไม่เป็นเชิงเส้นทำให้คำนวณง่าย ๆ ไม่ได้  เราจะต้องทำดังนี้ ….
🛑 ดึง raw data ความต้านทาน vs ความสว่างออกมาจากกราฟใน datasheet (B ในภาพ) ซึ่งงานแบบนี้ AI ถนัดมาก เพียงแค่โยนภาพกราฟนี้เข้า AI ก็จะได้ตัวเลข raw data ของภาพทั้งหมด
🛑 ความสัมพันธ์ที่ไม่เป็นเชิงเส้นระหว่างค่าความต้านทานกับค่าความสว่างจะต้องใช้ Power law เพื่อหาสมการระหว่างค่าความต้านทานกับค่าความสว่าง (Lux) …. รายละเอียดการคำนวณตามภาพล่าง


sensor ตัวต่อไปก็คือ Thermistor หรืออีกชื่อคือ NTC (Negative Temperature Coefficient) ตัวนี้มันจะเป็นตัวความต้านทานที่เปลี่ยนไปตามอุณหภูมิครับ  ซึ่ง Negative Temperature Coefficient หมายความว่าค่าความต้านทาน NTC จะลดลงเมื่ออุณหภูมิเพิ่ม  Thermistor ต้องใช้กับวงจรแบ่งแรงดันแบบเดียวกับ LDR  และมันไม่ได้ให้ค่าอุณหภูมิแบบเบ็ดเสร็จสมบูรณ์เหมือนกับ sensor DHT22 ที่กล่าวไปด้านบน  เราต้องทำการวัดค่าความต้านทานของ Thermistor เทียบกับอุณหภูมิที่เราจะใช้ในงานนั้น ๆ โดยเฉพาะ และออกแบบวงจรแบ่งแรงดันเพื่อให้แรงดันที่จะเข้า GPIO มีความเหมาะสมกับ code ที่เราเขียน

ในการใช้ Thermistor เพื่อแสดงค่าเป็น ℃  สมการจะมีความแตกต่างจาก LDR เนื่องจากค่าความต้านทานกับความร้อนของมันแปรผกผันและไม่เป็นเชิงเส้น และในทางคณิตศาสตร์แล้วการเปลี่ยนแปลงของอุณหภูมิ – ความต้านทานจะแตกต่างจาก Power law  ดังนั้นจึงต้องใช้สมการหนึ่งซึ่งทำงานได้ดีกับการแปรผกผันและไม่เป็นเชิงเส้น ก็คือสมการ Steinhart-Hart Equation

การคำนวณก็เริ่มจากใช้กราฟจาก datasheet ของ Thermistor จากเช่นเคยครับโดยนำกราฟมาหาค่าความต้านทานใน 3 ช่วงอุณหภูมิ อย่างของโปรเจคนี้ตั้งใจจะให้วัดในช่วง 0 – 100 ℃  ดังนั้นจึงเอาค่าความต้านทานที่ 0  50 และ 100 ℃ จากกราฟมาคิด และเมื่อแทนค่าลงใน Steinhart-Hart Equation แล้วก็จะได้เป็นค่า A B C รายละเอียดตามภาพครับ


ตัวอย่าง project LDR , Thermistor แสดงถึงค่าความต้านทานของ LDR , Thermistor ที่เปลี่ยนไปตามความสว่างและอุณหภูมิ

ESP32 code ของ project นี้

// Pin Definitions
const int LED_1 = 11;      // Controlled by GPIO 7 voltage
const int LED_2 = 13;      // Controlled by GPIO 8 voltage
const int ANALOG_PIN_1 = 7;
const int ANALOG_PIN_2 = 8;

// Voltage Thresholds
const float THRESHOLD_1 = 2.8;
const float THRESHOLD_2 = 2.5;

// Timing Constants
const unsigned long READ_INTERVAL = 300;  // 0.3 seconds in milliseconds
const unsigned long FLASH_INTERVAL = 100; // 5 Hz means 5 full cycles per second (100ms ON, 100ms OFF)

// Timing Variables
unsigned long lastReadTime = 0;
unsigned long lastFlashTime = 0;

// LED States
bool led1Flashing = false;
bool led2Flashing = false;
bool flashState = false;

// ESP32-S3 ADC Configuration
// By default, attenuation is set to 11dB (on Core v3.x), allowing a range of 0V to ~3.3V.
// The ADC resolution is 12-bit (0 to 4095).
const float ADC_VREF = 3.3;
const int ADC_MAX_VALUE = 4095;

void setup() {
  Serial.begin(115200);
  
  // Configure LED pins as outputs
  pinMode(LED_1, OUTPUT);
  pinMode(LED_2, OUTPUT);
  
  // Ensure LEDs start turned off
  digitalWrite(LED_1, LOW);
  digitalWrite(LED_2, LOW);

  // Configure ADC resolution
  analogReadResolution(12);
}

void loop() {
  unsigned long currentMillis = millis();

  // Task 1: Read Voltages every 0.3 seconds (300 ms)
  if (currentMillis - lastReadTime >= READ_INTERVAL) {
    lastReadTime = currentMillis;

    // Read raw ADC values
    int raw1 = analogRead(ANALOG_PIN_1);
    int raw2 = analogRead(ANALOG_PIN_2);

    // Convert raw values to voltage
    float voltage1 = (raw1 * ADC_VREF) / ADC_MAX_VALUE;
    float voltage2 = (raw2 * ADC_VREF) / ADC_MAX_VALUE;

    // Print to Serial Monitor for debugging
    Serial.printf("GPIO 7: %.2f V | GPIO 8: %.2f V\n", voltage1, voltage2);

    // Check thresholds to enable/disable flashing flags
    led1Flashing = (voltage1 >= THRESHOLD_1);
    led2Flashing = (voltage2 >= THRESHOLD_2);
  }

  // Task 2: Handle 5 Hz Flashing (Toggles state every 100 ms)
  if (currentMillis - lastFlashTime >= FLASH_INTERVAL) {
    lastFlashTime = currentMillis;
    flashState = !flashState; // Invert the common flash state flag

    // Handle LED 1 (GPIO 11)
    if (led1Flashing) {
      digitalWrite(LED_1, flashState);
    } else {
      digitalWrite(LED_1, LOW); // Force off if below threshold
    }

    // Handle LED 2 (GPIO 13)
    if (led2Flashing) {
      digitalWrite(LED_2, flashState);
    } else {
      digitalWrite(LED_2, LOW); // Force off if below threshold
    }
  }
}

ตัวอย่าง project LDR , Thermistor แสดงค่าความสว่างเป็น Lux และอุณหภูมิเป็น ℃

ESP32 code ของ project นี้

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

// --- Pin Allocations ---
#define LDR_PIN 1          // LDR Divider Input
#define NTC_PIN 5          // NTC Divider Input
#define I2C_SDA 40         // Custom I2C Data Pin
#define I2C_SCL 42         // Custom I2C Clock Pin

// --- Hardware Parameter Constants ---
#define VCC 3.38           // Supply Voltage as specified
#define LDR_RF 4700.0      // Fixed Resistor for LDR divider (4.7k)
#define NTC_RF 3300.0      // Optimized Fixed Resistor for NTC divider (3.3k)

// --- Derived Curve Mathematical Parameters ---
#define LDR_A 70000.0      
#define LDR_GAMMA 0.715    

// Calculated Steinhart-Hart Coefficients for the bottom curve
#define ST_A 0.0012215    
#define ST_B 0.0002443    
#define ST_C 0.000000346   

// --- OLED Display Settings ---
#define SCREEN_ADDRESS 0x3C
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1);

void setup() {
  Serial.begin(115200);

  // Initialize custom I2C interface pins for ESP32-S3
  Wire.begin(I2C_SDA, I2C_SCL);
  
  // Set ADC Resolution to 12-Bit (0-4095 range)
  analogReadResolution(12);

  // Initialize SH1106 Display Panel
  if(!display.begin(SCREEN_ADDRESS, true)) {
    Serial.println(F("SH1106 allocation failed"));
    for(;;);
  }
  
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.display();
}

void loop() {
  // ==========================================
  // 1. ASYNCHRONOUS LDR LUX CALCULATION
  // ==========================================
  int ldrAdc = analogRead(LDR_PIN);
  if (ldrAdc <= 0) ldrAdc = 1;
  if (ldrAdc >= 4095) ldrAdc = 4094;

  // Linearize to ADC Voltage Scale (~3.1V Internal Max Reference)
  float ldrVolt = (float)ldrAdc * (3.1 / 4095.0);
  float ldrResistance = LDR_RF * ((VCC / ldrVolt) - 1.0);
  float lux = pow((LDR_A / ldrResistance), (1.0 / LDR_GAMMA));

  // Limit bounds safety clamp
  if(lux < 0) lux = 0;

  // ==========================================
  // 2. ASYNCHRONOUS NTC TEMPERATURE CALCULATION
  // ==========================================
  int ntcAdc = analogRead(NTC_PIN);
  if (ntcAdc <= 0) ntcAdc = 1;
  if (ntcAdc >= 4095) ntcAdc = 4094;

  float ntcVolt = (float)ntcAdc * (3.1 / 4095.0);
  float ntcResistance = NTC_RF * ((VCC / ntcVolt) - 1.0);

  // Steinhart-Hart Implementation
  float logR = log(ntcResistance);
  float steinhartK = 1.0 / (ST_A + (ST_B * logR) + (ST_C * logR * logR * logR));
  float tempCelsius = steinhartK - 273.15;

  // ==========================================
  // 3. OLED MONITOR UPDATE RENDERING
  // ==========================================
  display.clearDisplay();
  
  // // Line 1: Temperature Reading (Decreased to Text Size 1)
  display.setTextSize(1);
  display.setCursor(0, 4); // Kept near the top
  display.print("Temp = ");
  display.print(tempCelsius, 1);
  display.print(" C");

  // Line 2: Illumination Level Reading (Decreased to Text Size 1)
  // Adjusted the cursors so the text flows tightly together on a smaller scale
  display.setCursor(0, 24);
  display.print("Brightness is =");
  display.setCursor(0, 36);
  display.print((int)lux);
  display.print(" Lux");

  display.display();

  // 1-second background refresh iteration delay
  delay(1000);
}