ชื่อโปรเจกต์: Smart Thermostat
เชื่อมต่อกับ Smart Thermostat ตัวนี้ได้ง่ายๆ แค่ส่งข้อความหาคุยกับมันตรงๆ ไม่ต้องไปวุ่นวายทำ Dashboard หรือหา Platform อื่นให้เหนื่อยเลยน้อง
ทำความรู้จักกับ Telegram Bots
Telegram เขามีชุด API ที่โคตรมีประโยชน์ให้น้องเอาไปยัดลงโปรเจกต์ได้เพียบ
น้องสามารถรัน Bot ไว้บนบอร์ด [Arduino](https://s.shopee.co.th/7fUgFAWSki) แล้วแชทสั่งงานมันได้เลย แค่ใช้ Library ง่ายๆ ที่ชื่อว่า Telegram Bot จะลงผ่าน Library Manager ในโปรแกรม Arduino IDE หรือจะ Import ไฟล์ .Zip ใน Arduino Web Editor ก็จัดไปวัยรุ่น
ถ้าอยากรู้วิธีจัดการ Telegram Bot บน MKR1000 แบบละเอียด ไปหาอ่านเพิ่มได้ แต่ใน Tutorial นี้พี่ขอข้ามไปก่อนนะ เดี๋ยวจะพาไปดูตอน Implement ลงใน Code จริงเลยว่าหล่อแค่ไหน
การจัดการเวลา (Time management)
เจ้า Thermostat ตัวนี้มันล้ำตรงที่น้องสามารถตั้งโปรแกรมล่วงหน้าได้ทั้งสัปดาห์แล้วสั่งให้มันวน Loop ไปเรื่อยๆ
วิธีทำก็คือ Thermostat จะทำการเรียก UDP call เพื่อเอาข้อมูลมาตั้งค่าให้กับ Real Time Clock (RTC) ในตัวมันเอง
ไปจัดการลง Library RTCZero และ WiFi101 ให้เรียบร้อย แล้วอัพโหลด Sketch นี้ไปลองเทสดูว่าเวลาเดินตรงไหม
#include <SPI.h>
#include <WiFi101.h>
#include <WiFiUdp.h>
#include <RTCZero.h>
RTCZero rtc;
WiFiUDP Udp; // สร้าง instance ของ UDP ไว้รับส่งข้อมูล
// ใส่ชื่อ WiFi กับ Password ของเราตรงนี้
char ssid[] = "xxxx"; // SSID ของน้อง
char pass[] = "yyyy"; // รหัสผ่าน WiFi
WiFiSSLClient client;
unsigned long epoch;
unsigned int localPort = 2390; // local port ที่ใช้รอฟัง UDP packets
IPAddress timeServer(129, 6, 15, 28); // time.nist.gov NTP server
const int NTP_PACKET_SIZE = 48; // NTP time stamp จะอยู่ใน 48 bytes แรกของข้อความ
byte packetBuffer[ NTP_PACKET_SIZE]; // buffer ไว้เก็บ packet เข้า-ออก
void setup() {
Serial.begin(115200);
// พยายามเชื่อมต่อ WiFi:
Serial.print("Connecting Wifi: ");
Serial.println(ssid);
while (WiFi.begin(ssid, pass) != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.println("");
Serial.println("WiFi connected");
rtc.begin();
GetCurrentTime();
}
void loop() {
Serial.print("Unix time = ");
Serial.println(rtc.getEpoch());
// ปริ้นวันที่ออกมาดูหน่อย...
Serial.print(rtc.getDay());
Serial.print("/");
Serial.print(rtc.getMonth());
Serial.print("/");
Serial.print(rtc.getYear());
Serial.print("\\t");
// ...ตามด้วยเวลา
print2digits(rtc.getHours());
Serial.print(":");
print2digits(rtc.getMinutes());
Serial.print(":");
print2digits(rtc.getSeconds());
Serial.println();
delay(1000);
}
void print2digits(int number) {
if (number < 10) {
Serial.print("0");
}
Serial.print(number);
}
void GetCurrentTime() {
int numberOfTries = 0, maxTries = 6;
do {
epoch = readLinuxEpochUsingNTP();
numberOfTries++;
}
while ((epoch == 0) || (numberOfTries > maxTries));
if (numberOfTries > maxTries) {
Serial.print("NTP unreachable!!");
while (1);
}
else {
Serial.print("Epoch received: ");
Serial.println(epoch);
rtc.setEpoch(epoch);
Serial.println();
}
}
unsigned long readLinuxEpochUsingNTP()
{
Udp.begin(localPort);
sendNTPpacket(timeServer); // ส่ง packet ไปหา time server
// รอเช็คว่ามีการตอบกลับมาไหม
delay(1000);
if ( Udp.parsePacket() ) {
Serial.println("NTP time received");
// ได้ packet มาแล้ว ก็อ่านข้อมูลข้างในเลย
Udp.read(packetBuffer, NTP_PACKET_SIZE);
// timestamp จะเริ่มที่ byte 40 ยาว 4 byte ดึงออกมาเป็น 2 word ก่อน:
unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
// รวมร่าง byte เป็น integer ตัวเดียว
// นี่คือเวลา NTP (วินาทีนับจาก 1 ม.ค. 1900):
unsigned long secsSince1900 = highWord << 16 | lowWord;
// แปลงเวลา NTP เป็นเวลาปกติ:
// Unix time เริ่ม 1 ม.ค. 1970 คิดเป็น 2208988800 วินาที:
const unsigned long seventyYears = 2208988800UL;
Udp.stop();
return (secsSince1900 - seventyYears);
}
else {
Udp.stop();
return 0;
}
}
// ส่ง NTP request ไปยัง time server ตาม address ที่ให้ไว้
unsigned long sendNTPpacket(IPAddress & address)
{
// เซ็ต byte ใน buffer ให้เป็น 0 ทั้งหมด
memset(packetBuffer, 0, NTP_PACKET_SIZE);
// เซ็ตค่าที่จำเป็นสำหรับ NTP request
packetBuffer[0] = 0b11100011; // LI, Version, Mode
packetBuffer[1] = 0; // Stratum
packetBuffer[2] = 6; // Polling Interval
packetBuffer[3] = 0xEC; // Peer Clock Precision
packetBuffer[12] = 49;
packetBuffer[13] = 0x4E;
packetBuffer[14] = 49;
packetBuffer[15] = 52;
// ตั้งค่าเรียบร้อย ส่ง packet ไปขอเวลาได้เลย!
Udp.beginPacket(address, 123); // NTP ใช้ port 123 นะน้อง
Udp.write(packetBuffer, NTP_PACKET_SIZE);
Udp.endPacket();
}
การบันทึกค่า (Saving settings)
แน่นอนว่าน้องคงไม่อยากให้ Thermostat ของน้อง "ความจำเสื่อม" ทุกครั้งที่ไฟดับหรือปิดเครื่องหรอกจริงไหม? :)
เพื่อแก้ปัญหานี้ เราจะเก็บข้อมูลไว้ใน Flash Memory ของบอร์ดโดยใช้ Library ที่ชื่อว่า FlashStorage ครับ
ในโปรเจกต์นี้เราจะใช้มันเก็บข้อมูลแบบ struct สำหรับตารางเวลา 7 วัน (7*24 ชั่วโมง) และค่าอุณหภูมิที่เราตั้งไว้
ลองอัพโหลดโค้ดตัวอย่างนี้ไปเทสดู จะได้รู้ว่ามันทำงานยังไง
/*
ตัวอย่างการเก็บและเรียกค่า integer จาก Flash memory
ค่าจะเพิ่มขึ้นเรื่อยๆ ทุกครั้งที่บอร์ด Restart
*/
#include <FlashStorage.h>
// จองพื้นที่ใน Flash memory สำหรับตัวแปร "int"
// แล้วตั้งชื่อว่า "my_flash_store"
FlashStorage(my_flash_store, int);
// ปล. พื้นที่ที่จองไว้จะหายไปนะถ้าเราอัพโหลด Sketch ใหม่ลงไปในบอร์ด
void setup() {
SERIAL_PORT_MONITOR.begin(9600);
int number;
// อ่านค่าจาก "my_flash_store" มาใส่ในตัวแปร "number"
number = my_flash_store.read();
// โชว์ค่าปัจจุบันใน Serial monitor หน่อย
SERIAL_PORT_MONITOR.println(number);
// บันทึกค่าใหม่ลงไป (บวกเพิ่มไป 1) สำหรับการรันครั้งหน้า
my_flash_store.write(number + 1);
}
void loop() {
// ไม่ต้องทำอะไร...
}
การอ่านค่าจาก [Sensor](https://s.shopee.co.th/7VBG2rX65j)
โปรเจกต์นี้เราใช้ Sensor ตระกูล DHT ที่วัดได้ทั้งความชื้นและอุณหภูมิ ซึ่งมันมี Library ของตัวเองนะ อย่าลืมไปโหลดมาให้ถูกตัวล่ะ
เนื่องจาก Code หลักมันมีหลายฟังก์ชัน พี่เลยแบ่งเป็น Tab จะได้ไม่มึน โค้ดที่เห็นข้างล่างนี้คือส่วนที่เอาไว้จัดการ Sensor ครับ
ใน Tab ที่ชื่อ Config เราจะประกาศ Class สำหรับ Sensor ไว้แบบนี้:
//#define USE_fahrenheit true // เอาคอมเมนต์ออกถ้าจะใช้ Fahrenheit แทน Celsius
class Sensor {
public:
Sensor(void);
void begin();
bool ReadSensors();
float GetTemp();
float GetHumidity();
private:
float t;
float f;
float h;
};
extern Sensor DHTSensor;
ส่วนอีก Tab เราก็จะมาเขียนรายละเอียดข้างใน Class กัน:
#include "DHT.h"
#include "config.h"
DHT dht(DHTPIN, DHTTYPE);
Sensor::Sensor(void) {
}
bool Sensor::ReadSensors() {
h = dht.readHumidity();
t = dht.readTemperature(); // อ่านอุณหภูมิแบบ Celsius (ค่าพื้นฐาน)
f = dht.readTemperature(true); // อ่านอุณหภูมิแบบ Fahrenheit
if (isnan(h) || isnan(t) || isnan(f)) { // เช็คหน่อยว่าอ่านค่าสำเร็จไหม ถ้าเจ๊งก็เด้งออก
Serial.println("Failed to read from DHT sensor!");
return false;
}
return true;
}
void Sensor::begin() {
dht.begin();
}
float Sensor::GetTemp() {
#ifdef USE_fahrenheit
return f;
#else
return t;
#endif
}
float Sensor::GetHumidity() {
return h;
}
อุปกรณ์ Hardware และ Library ที่ต้องใช้
เอาล่ะ ได้เวลาต่อสาย (Wiring) กันแล้วน้อง จำไว้ว่าหน้าจอ [LCD](https://s.shopee.co.th/6AfsSPcAnb) ต้องใช้ Library GFX และ ST7735 ส่วน Sensor DHT ก็ต้องใช้ DHT-sensor-library นะ จัดเตรียมให้ครบ อย่าให้ขาด!
รายละเอียดทางเทคนิคเพิ่มเติม
ระบบคุมอุณหภูมิแบบ Hysteresis
Smart Thermostat ตัวนี้ใช้การตรวจวัดอุณหภูมิที่แม่นยำและใช้ Algorithm แบบ "Deadband" เพื่อคุมการทำงานของเครื่องทำความร้อนหรือแอร์
- Sensing ความแม่นยำสูง: ใช้ Sensor DHT22 (AM2302) หรือ DS18B20 เพื่ออ่านอุณหภูมิห้อง
- Algorithm: เพื่อป้องกันไม่ให้ Relay มัน "สับรัว" (ทำงานติดๆ ดับๆ เมื่ออุณหภูมิแกว่งใกล้จุดที่ตั้งไว้) Firmware นี้เลยใช้ระบบ Hysteresis เข้ามาช่วย เช่น ถ้าตั้งเป้าไว้ 22°C มันอาจจะสั่งทำงานที่ 21.5°C และสั่งหยุดที่ 22.5°C เพื่อถนอมอุปกรณ์ยังไงล่ะน้อง