[Arduino]開發智慧型家電,紅外線篇
前言
在一個非常炎熱的夏天,下班回到家等待自己的是30度的高溫
除了眼神死依然還是眼神死,只能馬上把冷氣打開
那麼有沒有辦法在回家的路上就把冷氣給打開呢??
今天這篇文章就是分享給大家我是怎麼製作自己的智慧型家電
我是一個新手Arduino玩家,文章內容若有錯誤請多多包涵
準備材料
一顆紅外線接收器,台灣普遍家電遙控器使用的是 38KHz 的頻率,購買前請先確認好
一顆紅外線發射器,一樣要確認可以發射 38KHz 的頻率
兩顆 LED,我選擇買高亮綠光,可以直插3.3V
其他顏色的可能要確認是不是需要加電組
兩顆復位開關,開關就買自己喜歡的就好
非常好用的杜邦線,公+公 公+母 各一包
一顆 100歐姆 的電阻
一顆 NodeMCU CH340G,直接實現WIFI功能
以上價格請自行參考,我是在實體店買的,網路可能會比較便宜
概念
平常網路上搜尋到的紅外線教學
幾乎都是教你先利用接收器接收遙控器的紅外線頻率,再把數值寫死在程式碼中
再透過發射器送出頻率
這樣一來,整個裝置的靈活性會略顯不足
所以這次打算利用動態的方式來記錄紅外線頻率
簡單來說就是在不重新燒錄程式的形況下,更改需要的紅外線頻率
整體程式的流程圖大概如下(邏輯判斷往右邊方向的都是否定)
流程圖內有兩個按鈕,分別用來紀錄開啟及關閉的紅外線頻率
稍微理解過後我們就開始實作吧
硬體
從左至右分別是
- 紅外線接收器,左邊腳位接到板子的 D1 腳位,中間接地,右邊腳位接到 3.3V
- 紅外線發射器,正極接上 100歐姆 電阻後接到板子的 D2 腳位,負極接地
- 狀態一LED燈,正極接到板子的 D5 腳位,負極接地
- 狀態一按鈕,一個腳位接到板子的 D3 腳位,另一個腳位接地
- 狀態二LED燈,正極接到板子的 D6 腳位,負極接地
- 狀態二按鈕,一個腳位接到板子的 D4 腳位,另一個腳位接地
紅外線發射器不加電阻的下場就是...一股燒焦味發出
Arduino IDE 相關套件安裝
NodeMCU並不是隨插即用的,需要安裝一些東西
若第一次使用這張開發版可看這一篇 [Arduino]Arduino IDE 使用 NodeMCU
另外我們還需要一些套件,下載後放到 Arduino IDE 安裝目錄下的 libraries 目錄下
IRremoteESP8266,ESP8266使用的紅外線套件
Timer,計時器套件,可以實現定時執行的需求
記得將目錄名稱更改成以下圖片中的樣子
程式碼
有一部分是直接使用套件的程式碼範例,所以風格上有些不同
#include <Arduino.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <IRrecv.h>
#include <IRac.h>
#include <IRutils.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <Timer.h>
/* 紅外線發射 */
const uint16_t kIrLed = D2;
IRsend irsend(kIrLed);
/* 紅外線接收 */
const uint16_t kRecvPin = D1;
const uint16_t kCaptureBufferSize = 1024;
#if DECODE_AC
const uint8_t kTimeout = 50;
#else
const uint8_t kTimeout = 15;
#endif
const uint16_t kMinUnknownSize = 12;
IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true);
decode_results results;
/* 流程控制 */
const int offButtonPin = D3;
const int onButtonPin = D4;
const int offLedPin = D5;
const int onLedPin = D6;
uint8_t count = 0;
uint8_t statusCode = 0;
/* WIFI */
#ifndef STASSID
#define STASSID "WIFI的SSID"
#define STAPSK "WIFI的密碼"
#endif
char* ssid = STASSID;
char* password = STAPSK;
ESP8266WiFiMulti WiFiMulti;
/* 定時器 */
Timer t1;
/* 資訊變數 */
#ifndef MACADD
#define MACADD WiFi.macAddress() //用來當作唯一識別值
#endif
char* host = "遠端伺服器IP或網址(記得加上 http:// 或 https://)";
uint16_t port = 80; //一般網頁伺服器都是80port,視情況需要自行修改
void setup() {
irsend.begin();
Serial.begin(115200);
#if DECODE_HASH
irrecv.setUnknownThreshold(kMinUnknownSize);
#endif
irrecv.enableIRIn();
pinMode(offButtonPin, INPUT);
pinMode(onButtonPin, INPUT);
pinMode(offLedPin, OUTPUT);
pinMode(onLedPin, OUTPUT);
WiFi.mode(WIFI_STA);
WiFiMulti.addAP(ssid, password);
while (WiFiMulti.run() != WL_CONNECTED) {
delay(500);
}
t1.every(5000, chk_action); //五秒一次詢問伺服器有沒有指令需要執行
}
/*
送出紅外線頻率到伺服器儲存
*/
void send_data(char* type, uint16_t irr[], int irr_len) {
char* result;
if (join(result, ",", irr, irr_len) < 0) {
return;
}
char* path;
char* response;
if (type == "off") { //紀錄關閉的紅外線頻率
path = "/safe_ir_off.php";
}
if (type == "on") { //紀錄開啟的紅外線頻率
path = "/safe_ir_on.php";
}
char* url;
if(NULL != (url = (char*)malloc(strlen(host) + strlen(path) +1))){
url[0] = '\0';
strcat(url,host);
strcat(url,path);
} else {
return;
}
HTTPClient http;
http.begin(url);
http.addHeader("Content-Type", "application/json"); //json格式幾乎所有後端程式都可以支援
http.POST("{\"k\": \"" + MACADD + "\", \"v\":\"" + result + "\"}");
http.writeToStream(&Serial);
http.end();
free(url);
free(result);
}
int bytes_added(int result_of_sprintf) {
return (result_of_sprintf > 0) ? result_of_sprintf : 0;
}
/*
將數字陣列轉成文字
*/
int join(char*& result, char separator[], uint16_t data[], int loop_len) {
if (loop_len == 1) {
result = (char*)malloc(sizeof(uint16_t));
if (NULL == result) {
return -1;
}
sprintf(result, "%d", data[0]);
return 0;
}
int result_len = loop_len * 5 + ((loop_len -1) * strlen(separator)) + 1;
result = (char*)malloc(result_len);
loop_len -= 1;
if (NULL == result) {
return -1;
}
int i;
int length = 0;
for (i = 0; i < loop_len; i++) {
length += bytes_added(sprintf(result + length, "%d%s", data[i], separator));
}
sprintf(result + length, "%d", data[loop_len]);
return 0;
}
/*
檢查有沒有待執行的指令
*/
void chk_action(){
char* path = "/chk_action.php?k=";
char* url;
if(NULL != (url = (char*)malloc(strlen(host) + strlen(path) +1))){
url[0] = '\0';
strcat(url,host);
strcat(url,path);
} else {
return;
}
HTTPClient http;
http.begin(url + MACADD);
int httpCode = http.GET();
if (httpCode == 200) {
const char* data = http.getString().c_str();
uint16_t *rawData;
int len = 0;
if ((len = split(rawData, data)) < 0) {
return;
}
irsend.sendRaw(rawData, len, 38); //發射紅外線訊號
free(rawData);
}
http.end();
free(url);
}
/*
將文字切割成數字陣列
*/
int split(uint16_t* &result, const char* data) {
int len = strlen(data);
int count = 1;
for (int i = 0; i < len; i++) {
if (data[i] == ',') {
++count;
}
}
if (count == 1) {
return -1;
}
result = (uint16_t*)malloc(sizeof(uint16_t) * count);
if (NULL == result) {
return -1;
}
int index = 0;
int sum = 0;
for (int i = 0; i < len; i++) {
switch (data[i]) {
case '0':
sum = sum * 10;
break;
case '1':
sum = sum * 10 + 1;
break;
case '2':
sum = sum * 10 + 2;
break;
case '3':
sum = sum * 10 + 3;
break;
case '4':
sum = sum * 10 + 4;
break;
case '5':
sum = sum * 10 + 5;
break;
case '6':
sum = sum * 10 + 6;
break;
case '7':
sum = sum * 10 + 7;
break;
case '8':
sum = sum * 10 + 8;
break;
case '9':
sum = sum * 10 + 9;
break;
default:
if (sum > 65535) {
free(result);
return -1;
}
if (data[i] == ',') {
*(result + index) = (uint16_t)sum;
++index;
sum = 0;
}
}
if (sum > 65535) {
free(result);
return -1;
}
}
if (sum > 65535) {
free(result);
return -1;
}
*(result + index) = (uint16_t)sum;
return count;
}
void loop() {
/*
接收紅外線訊號狀態
*/
if (statusCode == 1 || statusCode == 2) {
if (statusCode == 1) {
digitalWrite(offLedPin, LOW);
delay(500);
digitalWrite(offLedPin, HIGH);
delay(500);
}
if (statusCode == 2) {
digitalWrite(onLedPin, LOW);
delay(500);
digitalWrite(onLedPin, HIGH);
delay(500);
}
if (irrecv.decode(&results)) {
if (results.overflow)
Serial.printf(
"WARNING: IR code is too big for buffer (>= %d). "
"This result shouldn't be trusted until this is resolved. "
"Edit & increase kCaptureBufferSize.\n",
kCaptureBufferSize);
int len = getCorrectedRawLength(&results);
uint32_t index = 0;
uint16_t irr[len];
for (int i = 1, len2 = (&results)->rawlen; i < len2; i++) {
uint32_t usecs;
for (usecs = (&results)->rawbuf[i] * kRawTick; usecs > UINT16_MAX; usecs -= UINT16_MAX) {
irr[index] = UINT16_MAX;
index += 1;
irr[index] = 0;
index += 1;
}
irr[index] = usecs;
index += 1;
}
yield();
if (statusCode == 1) {
send_data("off", irr, sizeof(irr) / sizeof(uint16_t));
digitalWrite(offLedPin, LOW);
} else {
send_data("on", irr, sizeof(irr) / sizeof(uint16_t));
digitalWrite(onLedPin, LOW);
}
statusCode = 0;
}
return;
}
if (digitalRead(offButtonPin) == LOW) {
count += 1;
delay(500);
if (count == 3) {
statusCode = 1;
count = 0;
return;
}
} else if (digitalRead(onButtonPin) == LOW) {
count += 1;
delay(500);
if (count == 3) {
statusCode = 2;
count = 0;
return;
}
} else {
count = 0;
}
t1.update();
}
伺服器端程式碼
伺服器的語言我選擇使用 PHP,最主要的原因就是簡單方便
而且許多免費的網路空間幾乎都支持 PHP
不知道怎麼架站的開發者可以參考這篇文章
以下將會牽扯到網頁後端開發的領域,
若對這不熟悉的讀者可以直接複製程式碼使用
接下來我們會需要四隻PHP檔案
- index.php 使用者首頁,會在這頁面控制設備
- chk_action.php 讓設備查詢有無待執行的指令
- safe_ir_off.php 將設備送來的紅外線頻率(關閉)記錄起來
- safe_ir_on.php 將設備送來的紅外線頻率(開啟)記錄起來
並且在根目錄新增一個 data 目錄
因為後端邏輯相對簡單,所以我就不另外畫流程圖了,直接文字敘述
一開始遇到的難題就是,後端並不知道傳送過來的資料是屬於哪一個設備
如果是手機APP,可以直接用手機的唯一識別碼來當作Key
但偏偏這塊板子並沒有這種東西...
最後左思右想,想到了網卡的 MAC地址 也是每張不一樣且唯一的(理論上)
那麼就利用 MAC地址 當作設備的Key,儲存的方式選擇最簡單的寫入Json檔
Json檔中每一個Key就是一個指令,待執行指令會寫在 action 中
safe_ir_on.php
<?php
$input = json_decode(file_get_contents('php://input'), true);
/*
檢查必要參數
*/
if (isset($input['k']) === false || isset($input['v']) === false) {
exit;
}
$k = str_replace(':', '-', trim($input['k']));
$v = trim($input['v']);
/*
排除空資料
*/
if ($k == '' || $v == '') {
exit;
}
/*
獲取資料
*/
$path = 'data/'.$k;
if (file_exists($path) === true) {
try {
$data = json_decode(file_get_contents($path), true);
} catch (Exception $e) {
$data = [];
}
} else {
$data = [];
}
/*
寫入紅外線頻率(開啟)
*/
$data['on'] = $v;
file_put_contents($path, json_encode($data));
?>
safe_ir_off.php
<?php
$input = json_decode(file_get_contents('php://input'), true);
/*
檢查必要參數
*/
if (isset($input['k']) === false || isset($input['v']) === false) {
exit;
}
$k = str_replace(':', '-', trim($input['k']));
$v = trim($input['v']);
/*
排除空資料
*/
if ($k == '' || $v == '') {
exit;
}
/*
獲取資料
*/
$path = 'data/'.$k;
if (file_exists($path) === true) {
try {
$data = json_decode(file_get_contents($path), true);
} catch (Exception $e) {
$data = [];
}
} else {
$data = [];
}
/*
寫入紅外線頻率(關閉)
*/
$data['off'] = $v;
file_put_contents($path, json_encode($data));
?>
chk_action.php
<?php
/*
設備利用 200 狀態碼判定是否執行
故不須執行時回傳 200 以外的狀態碼
*/
function stop() {
header('Status: 400 Bad Request');
exit;
}
/*
檢查必要參數
*/
if (isset($_GET['k']) === false) {
stop();
}
$k = str_replace(':', '-', trim($_GET['k']));
/*
檢查資料存在
*/
$path = 'data/'.$k;
if (file_exists($path) === false) {
stop();
}
try {
/*
獲取資料
*/
$data = json_decode(file_get_contents($path), true);
if (isset($data['action']) === false || $data['action'] == '') {
stop();
}
$v = $data['action'];
/*
清空指令後存檔
*/
$data['action'] = '';
file_put_contents($path, json_encode($data));
/*
回傳資料給設備
*/
echo $v;
} catch (Exception $e) {
stop();
}
?>
index.php
<?php
/*
獲取設備資料
*/
$list = [];
foreach (glob("data/*") as $file_name) {
try {
$data = json_decode(file_get_contents($file_name), true);
$name = str_replace('-', ':', basename($file_name));
foreach ($data as $key => $value) {
if ($key == 'action') {
continue;
}
$list[$name][] = $key;
}
rsort($list[$name]);
} catch (Exception $e) {
}
}
/*
寫入指令
*/
if (isset($_POST['type']) && isset($_POST['k'])) {
$k = str_replace(':', '-', $_POST['k']);
/*
獲取資料
*/
$path = 'data/'.$k;
if (file_exists($path) === true) {
try {
$data = json_decode(file_get_contents($path), true);
if (isset($data[ $_POST['type'] ])) {
$data['action'] = $data[ $_POST['type'] ];
file_put_contents($path, json_encode($data));
}
} catch (Exception $e) {
}
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>智慧型家電控制面板</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<div class="row justify-content-around">
<div class="col-8">
<table class="table">
<thead>
<tr>
<th scope="col">設備ID</th>
<th scope="col">功能</th>
</tr>
</thead>
<tbody>
<?php foreach ($list as $id => $func): ?>
<tr>
<th scope="row"><?php echo $id; ?></th>
<td>
<form method="POST">
<input type="hidden" name="k" value="<?php echo $id; ?>" />
<?php foreach ($func as $name): ?>
<button type="submit" class="btn btn-success" name="type" value="<?php echo $name; ?>"><?php echo $name; ?></button>
<?php endforeach; ?>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
範例影片
可以看到紅外線頻率變成一連串的數字儲存在網站內
這麼一來,設備就可以隨時的替換掉紅外線頻率,而且不需要重新燒錄,非常的方便
可改進部分及可擴充部分
這是一個非常簡單的小作品,使用到的材料不多
功能也很陽春,只是簡單的開啟或關閉
但是可以擴充的部分也不少
比如可以加個溫度傳感器
在室溫上升至XX度時開啟冷氣、下降到XX度時關閉冷氣
又比如現在是用兩顆按鈕來代表兩種狀態,可以加一塊螢幕顯示器
並且修改一下程式碼,利用螢幕顯示的方式讓使用者自訂狀態名稱來記錄頻率
有各式各樣的想法可以去實現看看,Arduino真的很好玩
結尾
這是我第一次從頭到尾完成整個作品,也是一個很好玩的經驗
這期間經歷了很多難關,但都一一克服了,成就感十足
也非常感謝社團、群組、朋友們的幫忙
另外在這邊也建議讀者們可以去嘗試學習一下網站開發
因為硬體搭配上軟體可以讓整個侷限性大大擴張
讓一個設備更好的融入到生活上
若這邊教學有幫助到你的話~請多多分享轉發出去給更多的人知道
謝謝大家的觀看