增加安全性的方法基本上有三種 : 更改 MQTT 預設 port 、增加使用者名稱及密碼、TLS安全加密
(TLS 安全加密因為目前我使用的 PubsubClient Library 似乎沒有支援,先暫時擱置)
要使用時必須先修改 mosquitto 的設定檔
mosquitto 的設定檔位於 /etc/mosquitto/mosquitto.conf 可以用以下指令進行編輯
在編輯之前請先複製一份,以免搞砸了(我花了兩個多小時處理我搞砸了設定檔這回事)
sudo su
cd /etc/mosquitto
cp mosquitto.conf mosquitto_bp.conf
複製完成後就可以來編輯設定檔了
sudo nano /etc/mosquitto/mosquitto.conf
打開後大概會長如下圖
你可能會跟我長的不太一樣(因我的原始檔被我刪除後弄不回來了,嘗試刪除 mosquitto 並且有加入把設定檔刪除的指令,重新安裝後它還是不會自己生成,不曉得為什麼)
總之最後我是用如上圖的設定檔,才把 mosquitto 給救回來,花了我兩個小時 ....
--------------------------------------------------------------------------------------------
增加安全性的方法基本上有三種 : 更改 MQTT 預設 port 、增加使用者名稱及密碼、TLS安全加密
前兩種較簡單,先實做看看
1. 更改 MQTT 預設 port
MQTT 預設 port 為 1883 ,更改掉這個數字可以簡易的防範有心人滲入
更改的方式很簡單,在 /etc/mosquitto/mosquitto.conf 內加入文字
port 61883
就可以將 MQTT 的預設 port 改為 61883
修改完後,保存退出就可以。執行
service mosquitto restart
就可以重啟 mosquitto
因為修改了預設 port。 在進行發佈以及接收訊息時要額外加入 -p 指名 port 為多少,如下
(若在此沒有修改,會出現 Error: Connection refused 的報錯)
Raspberry 端
訂閱
mosquitto_sub -t /leds/esp8266 -p 外部連接port
因為修改了預設 port。 在進行發佈以及接收訊息時要額外加入 -p 指名 port 為多少,如下
(若在此沒有修改,會出現 Error: Connection refused 的報錯)
Raspberry 端
訂閱
mosquitto_sub -t /leds/esp8266 -p 外部連接port
發佈
mosquitto_pub -t /leds/esp8266 -p 外部連接port -m "訊息 .... "
NoduMCU 部分的程式碼,就將前一篇所使用的程式碼,其中的 #define MQTT_PORT 1883 改為 61883 就行了
Hint2 : 我依照參考連結打開 /etc/mosquitto/mosquitto.conf 後發現裡面的文字很少,不像他的裡面有一長串。我一直以為是因為新版的 mosquitto 更改了放置設定檔的地方,後來發現好像不是這麼一回事,而是參考連結是把可以修改的參數都放進去,並且把想要使用的功能的 # 去掉。
經過我搞砸了一次後,我認為我還是加入自己要的東西就好,至於那些不想要加入的功能就別放進文件裡了
2. 增加使用者帳號及密碼
預設的 mosquitto 是允許我們匿名登入的,所以在前一篇時我們不需要輸入帳號密碼就可以對 topic 的數值進行接收與傳送。但很顯然的這可能會有安全性的問題,所以我們最起碼要新增一個使用者帳號 & 密碼
2.1 新增一個新的使用者帳號 & 密碼
mosquitto_passwd -c /etc/mosquitto/passwd test
上述指名新增一個使用者帳號 名稱為 test 並將這個文件存放在 /etc/mosquitto/passwd,接著會自動跳出來要輸入要設定的密碼(我同樣設為 test)
如果要新增第二個使用者帳號 & 密碼 使用
mosquitto_passwd /etc/mosquitto/passwd test2
如果要新增第二個使用者帳號 & 密碼 使用
mosquitto_passwd /etc/mosquitto/passwd test2
補充 : 刪除一個用戶可以用
mosquitto_passwd -D /etc/mosquitto/passwd test
2.2 回到 /etc/mosquitto/mosquitto.conf 加入以下兩行, 禁用匿名登入,並指名帳號密碼所存放的路徑
allow_anonymous false
password_file /etc/mosquitto/passwd
service mosquitto restart
2.4 查看是否為 active(running) 而不是 active(exited)
service mosquitto status
Hint : 若在此有出現報錯或出現 active(exited) 都代表設定失敗,可以參考以下解決
(1) 嘗試執行 mosquitto -c /etc/mosquitto/mosquitto.conf
如果無法執行,會報錯是哪裡有錯。
(2) 將 log_dest none 改為
log_dest file /var/log/mosquitto/mosquitto.log
此時在 Rpi 的訂閱要改為使用
mosquitto_sub -t /leds/esp8266 -p 61883 -u test -P test
發佈要改為使用
mosquitto_pub -t /leds/esp8266 -p 61883 -u test -P test -m "TEST message"
-u 後面接的是使用者名稱;-P 接的是使用者密碼
Hint : 小寫的u , 大寫的 P 。大小寫是有區別的
後來發現可能是原本使用的那個函式庫有點問題,我換了一個函式庫使用
同樣可以在 Arduino IDE 裡面的 "管理函式庫" 輸入 mqtt 進行下載
名字為 PubSubClient
程式碼的部分有大量的改動,實現了一些額外的功能,包含前面一篇所有的共有
1. NodeMCU 按按鈕使 LED 燈反轉,並將目前燈號狀態發佈至 /leds/esp8266 (ex. "Led trun ON by NodeMCU")
2. 一個 switch 以及 一個 Muti choose 用來直接控制 NodeMCU 上的 LED 燈(發佈訊息至 /leds/esp8266 ex. "ON" , "OFF")。此功能是藉由 NodeMCU 訂閱 /leds/esp8266 所達成的 (偵測到該 topic 的數據變成 ON 就點亮 LED)
3. 新增一個 topic 為 /numbers/out ,可以在手機上面直接修改這個 topic 目前的值(int or float), NodeMCU 亦訂閱了這個 topic 。當 NodeMCU 偵測到這個 topic 的值被改變時,會將這個値抓取下來,原封不動的再發佈到 /numbers/back。
4. 呈第 3 點 ,從 /numbers/out 抓取到的值(為一個字串)會經由程式將其轉換為 float 以及 int 的格式。
5. 利用 timer 將變數 number 每秒累加一次,並發佈到 /numbers/esp8266
6. 呈第 1 點,當按鈕按下時不僅 LED 反轉以及發佈燈號狀態至 /leds/esp8266 ,同時也發佈按鈕被按過的次數至 /times/esp8266 。且該數值是被 retain 的(也就是當新的訂閱者出現時,就算目前沒有新的發布訊息,也可以得知該 topic 的最後一筆數據)
後記 : 為什麼我實現這六大功能? 因為只要懂了這六大功能,基本上常見的應用都已經囊括在內了,舉例來說,我擁有 10 個 NodeMCU 全部都連上我的 MQTT 架構
針對 1 : 我們就可以知曉 "任何一個 NodeMCU" 的 LED 燈狀態。當然這不僅限於針對 LED 燈而已,事實上實現了這樣的功能,等同我們可以知曉任何一塊 NodeMCU 裡面的任何一個 digital I/O 腳位的狀態
這裡的"我們"不只是說可以由手機去查看而已, 還包含這 10 個 NodeMCU 彼此都可以知道彼此的 I/O 狀態,例如放在家中大門口的 NodeMCU 可以發佈目前的家中大門是否開啟,若被開啟了就將狀態發佈上去 MQTT ,而在你房間內的 NodeMCU 藉由訂閱這個 topic 也會知曉目前大門已經被開啟了,手機當然也會知道目前大門被開啟了
針對 2 : 設計手機對 topic 發送訊息來操作某個 I/O 腳位,例如我們可以用手機對 topic 發送 "turn on the light" ,而在你家後院的 NodeMCU 就可以開啟後院的燈
光以上兩點,已經可以實現八成以上的應用(又或者說幾乎所有具實用性的應用都會使用到這兩個功能)
針對 3 : 這個應用屬於第 2 點的延伸應用,有些時候我們不只是想要點亮燈而已,這個燈或許有亮度可以調整的。舉例來說我們可以用手機發佈 "30" 的訊息,透過程式上的撰寫,可以讓後院的燈以 30% 的亮度點亮。
當然這不僅限於燈,也可能可以是顆馬達,你可以透過 "1500" 遠端設定馬達的轉速為 1500 rpm 等等
針對 4 : 此點是承襲於 2, 3 點所製的,因 MQTT 在做訊息傳遞時是以 "字串" 來做傳遞,而我們在 Arduino 中對於程式的撰寫,可能會需要一個 int 或 float 的數據形態來進行判斷才行,第 4 點所實做的功能是從 topic 上接收一個字串 -> 轉為 int 或 float 的數據形態 -> 再次轉為字串的形態發送到另一個 topic
所以我們實現了
4.1 可以從 topic 上的字串將命令轉為 int 或 float 以供我們在 NodeMCU 進行程式上的撰寫
4.2 可以將一個 int 或 float 的數值上傳到 topic
針對 5 : 實現以 timer 來進行每秒傳送一個數值的功能,這個不僅可以用來當作判定該 NodeMCU 現在是否仍然連線,也可以更改一下程式的變數就成為每秒發佈感測器數據的程式碼。(同時由結果看出,無論是 int 或是 float 的數值都可以成功的被發佈上去)
針對 6 : 前面我們已經實現將 int 形態或 float 型態發佈上 topic 的功能,為什麼還要做第 6 點呢? 其原因在於前面的發布訊息都沒有使用到 retain 的功能。
舉個例子,在我程式碼中我將按鈕被按下的次數發佈到 /times/esp8266 ,所以當我們每次按下按鈕時該 topic 的數值都會加一。這看似沒什麼問題,我可以在手機的介面上不斷的看到 /times/esp8266 的數值隨著每次按下按鈕就加一。
假如我現在按鈕按到 10 次,然後我將手機的 MQTT Dash 退出,此時我再多按 3 下按鈕
然後再次登入 MQTT Dash。問題出現了,我的手機介面上顯示 /times/esp8266 依舊為10,而不是我們預期的 13 。然後我再按下一次 NodeMCU 上的按鈕 ,手機顯示 /times/esp8266 為 14
這就是我說的數據沒有被 retain 時會發生的問題。
當我們手機再次登入 MQTT Dash 時,它並不會去更新目前 topic 的值是什麼,直到該 topic 有一個新的發布訊息出現。
解決這個問題的方式為在 "發佈" 訊息的時候就告知我的 broker(Raspberry pi) 這個訊息必須被 retain ,這樣一來當我們手機使用 MQTT Dash 登入上我們的 MQTT 時,Broker 會認為出現了一個新的訂閱者,就會將該 topic 的 last message 傳送給他,而不是等到有人對該 topic 發佈了最新訊息時。
程式碼
#include "ESP8266WiFi.h"
#include "PubSubClient.h"
#include "SimpleTimer.h"
const char* WLAN_SSID = "wifi名稱";
const char* WLAN_PASS = "wifi密碼";
const char* mqttServer = "192.168.0.105"; // MQTT伺服器位址
// 若你的
const int MQTT_PORT = 61883;
const char* mqttUserName = "使用者名稱";
const char* mqttPwd = "使用者密碼";
const char* clientID = "NodeMCU"; // 用戶端ID,隨意設定。
// Topic 設置
const char* nTopic = "/numbers/esp8266";
const char* LTopic = "/leds/esp8266";
const char* numOutTopic = "/numbers/out";
const char* numBackTopic = "/numbers/back";
const char* buttonTopic = "/times/esp8266";
const int LED_PIN = D3;
#define BUTTON_PIN D2
int timer_id;
// 暫存MQTT訊息字串
String msgStr = "";
float number = 0.2;
int button_times = 0;
// Callback function header
void callback(char* topic, byte* payload, unsigned int length);
WiFiClient espClient;
PubSubClient MqttClient(mqttServer,MQTT_PORT,callback,espClient);
SimpleTimer timer;
// 當有任何一個已 subscribe 的 topic 有被發佈最新訊息時會進入此函式
void callback(char* topic, byte* payload, unsigned int length) {
// 如果 topic 名稱為 LTopic (也就是前面定義的 "/leds/esp8266" )
if ( strcmp(topic,LTopic) == 0 ){
// 如果訊息為 ON
if (memcmp(payload, "ON", 2) == 0) {
digitalWrite(LED_PIN, HIGH);
}
else if (memcmp(payload, "OFF", 3) == 0) {
digitalWrite(LED_PIN, LOW);
}
else if (memcmp(payload, "TOGGLE", 6) == 0) {
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
}
// 如果 topic 名稱為 numOutTopic
if ( strcmp(topic,numOutTopic) == 0 ){
byte* p = (byte*)malloc(length);
memcpy(p,payload,length);
p[length] = '\0';
String S = String((char*)p);
float payloadF = S.toFloat(); //將收到的訊息轉成 float 型態
int payloadI = S.toInt(); //將收到的訊息轉成 int 型態
Serial.printf("payload in float = %1.1f\n",payloadF);
Serial.printf("payload in int = %d\n",payloadI);
MqttClient.publish(numBackTopic, p, length,true); //將收到的值原封不動傳回至 numBackTopic(且retain)
MqttClient.publish(numBackTopic1, p, length); //將收到的值原封不動傳回至 numBackTopic1
free(p); // Free the memory
}
Serial.println("callback function done");
}
void setup_wifi() {
while (WiFi.status() != WL_CONNECTED) {
Serial.print("Connecting to ");
Serial.println(WLAN_SSID);
WiFi.begin(WLAN_SSID, WLAN_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.println("WiFi connected");
Serial.println("Local IP address: "); Serial.println(WiFi.localIP());
}
Serial.println("");
Serial.println("WiFi connected");
}
void Reconnect() {
if (MqttClient.connected()) {
return;
}
while (!MqttClient.connected()) {
if (MqttClient.connect(clientID, mqttUserName, mqttPwd)) {
Serial.println("MQTT connected");
} else {
Serial.print("failed, rc=");
Serial.print(MqttClient.state());
Serial.println(" try again in 5 seconds");
delay(5000); // 等5秒之後再重試
}
}
}
/* =====================*/
// ===== timer_1000 ====
/* =====================*/
void timer_1000(){
number++; // 每秒 number 自累加 1
msgStr = msgStr + number; // number 轉 String
byte arrSize = msgStr.length() + 1;
char msg[arrSize];// 宣告字元陣列
Serial.print("Publish message: ");
Serial.println(msgStr);
msgStr.toCharArray(msg, arrSize); // 把String字串轉換成字元陣列格式
MqttClient.publish(nTopic, msg); // 發布 number(已轉換為msg) 至 nTopic
msgStr = "";
timer.restartTimer(timer_id);
}
void setup() {
pinMode(LED_PIN,OUTPUT);
pinMode(BUTTON_PIN, INPUT);
Serial.begin(115200);
setup_wifi();
MqttClient.setServer(mqttServer, MQTT_PORT);
timer_id = timer.setInterval(1000L, timer_1000); //設定timer每1000ms觸發一次
if (!MqttClient.connected()) { Reconnect(); }
}
void loop() {
timer.run();
int button_first = digitalRead(BUTTON_PIN);
if (!MqttClient.connected()) { Reconnect(); }
MqttClient.loop();
MqttClient.subscribe(numOutTopic,1); //訂閱 numOutTopic 這個 topic 且品質為 QoS1
MqttClient.loop(); //必須在每個 subscribe 後面加上MqttClient.loop();
MqttClient.subscribe(LTopic,1);
MqttClient.loop();
delay(10);
MqttClient.loop(); //頻繁的使用 MqttClient.loop(); 可以有效的改善對 topic 訊息訂閱(接收)的反應速度
int button_second = digitalRead(BUTTON_PIN); // 讀取當前 BUTTON 狀態
if ((button_first == HIGH) && (button_second == LOW)) { //代表按鈕被按下
button_times++; // button 被按下的次數加 1
Serial.printf("Button is pressed %d times",button_times);
msgStr = msgStr + button_times; // number 轉 String
byte arrSize = msgStr.length() + 1;
char msg[arrSize];
Serial.print("Publish message: ");
Serial.println(msgStr);
msgStr.toCharArray(msg, arrSize);
MqttClient.publish(buttonTopic, msg, true); //將按鈕被按的次數發佈到 buttonTopic(且為retain)
msgStr = "";
digitalWrite(LED_PIN,!digitalRead(LED_PIN)); //反轉 LED 燈狀態
if (digitalRead(LED_PIN) == HIGH){ //讀取目前 LED 燈狀態
MqttClient.publish(LTopic,"Led trun ON by NodeMCU"); //若目前 LED 亮
}
else{
MqttClient.publish(LTopic,"Led trun OFF by NodeMCU"); //若目前 LED 滅
}
}
}
若你想要保持按鈕被按過的次數,例如你並不想要每次 NodeMCU 重新啟動,再次按按鈕就把 /times/esp8266 的次數又從 1 開始數,可以加入下方的程式碼 (在程式一開始的時候抓取 topic 的 retained message)
在 void callback(char* topic, byte* payload, unsigned int length) 函式裡面加入
// 如果 topic 名稱為 buttonTopic
if ( strcmp(topic,buttonTopic) == 0 ){
byte* p = (byte*)malloc(length);
memcpy(p,payload,length);
p[length] = '\0';
String S = String((char*)p);
int payloadI = S.toInt(); //將收到的訊息轉成 int 型態
Serial.printf("button pressed times(retain) = %d\n",payloadI);
button_times = payloadI;
free(p); // Free the memory
}
接著將 void setup() 改為
void setup() {
pinMode(LED_PIN,OUTPUT);
pinMode(BUTTON_PIN, INPUT);
Serial.begin(115200);
setup_wifi();
MqttClient.setServer(mqttServer, MQTT_PORT);
while (!MqttClient.connected()) { Reconnect(); }
MqttClient.subscribe(buttonTopic,1);
MqttClient.loop();
MqttClient.unsubscribe(buttonTopic);
MqttClient.loop();
timer_id = timer.setInterval(1000L, timer_1000); //設定timer每1000ms觸發一次
}
// 備註 : 當 MqttClient.subscribe 被執行時將會進入 void callback(char* topic, byte* payload, unsigned int length)
如此一來每當 NodeMCU 重開機時,就會先去抓取 /times/esp8266 的值並且覆蓋掉程式裡面的全域變數 button_times,這樣按按鈕時就不會歸零重新計數了。並且當我們抓完目前在 topic 的值以及覆蓋後,就取消對這個 topic 的訂閱,這樣子才不會發生我們之後對這個 topic 發佈最新的按鈕次數訊息,卻又觸發了訂閱,一來一往會造成數值來不及反應的倒退行為
關於更多的增加安全性功能,例如 ACL( 設定 user帳號的權限 ) 可見此文
補充 :
若要刪除所有 topic 的數據( or message) ,包含 retained message 可使用
sudo su
sudo service mosquitto stop
sudo rm /var/lib/mosquitto/mosquitto.db
sudo service mosquitto start
參考連結
PubSubClient API Documentation
subscribe more than 5 topics in sequence
MQTT Client Library Encyclopedia – Arduino PubSubClient
【轉載】MQTT的學習之Mosquitto集羣搭建
Multiple MQTT Topics with Arduino PubSubClient
MQTT username and passowrd
MQTT教學(五):「保留」發布訊息以及QoS品質設定
MQTT教學(九):使用ESP8266上傳資料到ThingSpeak MQTT伺服器
How do I set authentication for a Mosquitto Broker?
Mqtt精髓系列之保留消息Retained Messages
publish fails when including retained parameter
Mosquitto MQTT 安裝
mosquitto_sub man page
RASPBERRY PI TALKING TO ESP8266 USING MQTT
mqtt-tls.ino
Convert/Reading payload to int, float, and String / better solution?
Arduino 基本語法筆記
C 語言標準函數庫分類導覽 - string.h memcpy()
C語言學習筆記 (008) - C語言字符串操作總結大全(超詳細)(轉)