文章程式碼顯示

2018年7月13日 星期五

一起學 Python 115 : Rapsberry pi 與 NodeMCU (ESP8266) 溝通 基於 MQTT - 2 安全性



增加安全性的方法基本上有三種 : 更改 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

發佈
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 -D /etc/mosquitto/passwd test

2.2 回到 /etc/mosquitto/mosquitto.conf 加入以下兩行, 禁用匿名登入,並指名帳號密碼所存放的路徑

allow_anonymous false
password_file /etc/mosquitto/passwd

2.3 重啟mosquitto

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 。大小寫是有區別的


這部分我的 NodeMCU 一直無法經過設定使用者帳號密碼的方式成功的接收(發佈)訊息,改了很多次程式碼也沒有成功,但在 Rpi 以及手機都確定是可以發佈(接收)。

後來發現可能是原本使用的那個函式庫有點問題,我換了一個函式庫使用

同樣可以在 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語言字符串操作總結大全(超詳細)(轉)

↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引