2022年11月6日 星期日

三、1單向一對一/點對點(peer to peer)架構

 三、使用範例與說明:

在本章節中將會透過幾個範例為讀者示範如何去實現及實作前面一節中不同拓樸組態的配置,此外在我們的範例程式中,不需要特別去區分所是用的模組是ESP8266或是ESP32,單一程式可以無落差的直接套用在兩種模組上,這也是我們這個範例程式最大的特點之一!除此之外,為了避免在傳輸發送前的配對還必須先得知接收模組的MAC位址,在我們的程式中都會採用廣播式的方式發送資料,因此便可省略了必須先知悉接收裝置MAC位址碼的問題!


三、1單向一對一/點對點(peer to peer)架構

在這個小節中將示範二、1 一對一單向傳輸』這個ESP-NOW最基本拓樸結構的通信方式,而在介紹範例程式之前,先讓我們來看一下在ESP-NOW常會使用到的一些指令函數,以下便是 ESP-NOW 最基本功能的幾個指令函數摘要: 

函數名稱和說明

esp_now_init()初始化 ESP-NOW。但是你必須在初始化 ESP-NOW 之前先初始化 Wi-Fi功能如果成功,則返回 0。

  esp_now_set_self_role(role)自我腳色設定函式role這個參數可以是:ESP_NOW_ROLE_IDLE=0ESP_NOW_ROLE_CONTROLLERESP_NOW_ROLE_SLAVEESP_NOW_ROLE_COMBOESP_NOW_ROLE_MAX

  esp_now_add_peer(uint8 mac_addr, uint8 role, uint8 channel, uint8 key, uint8 key_len)呼叫此函數可用來配對設備。

  esp_now_send(uint8 mac_address,uint8 data,int len)資料數據發送函式用來發送data這部分的數據。

  esp_now_register_send_cb(onDataSend)註冊一個在發送數據時觸發的回應函數(即onDataSend)發送訊息時會呼叫這個函數而此函數可用以返回發送是否成功的訊息。

  esp_now_register_rcv_cb(onDataRecv)註冊一個接收數據時觸發的回應函數(即onDataRecv)。當通過 ESP-NOW 接收到數據時,會呼叫這一個函數。


◎ 發送端程式列表與說明

本次範例所使用的開發工具還是採用最常見的Arduino IDE在撰寫ESP-NOW程式時有兩個問題最讓人頭痛一是要先查出接收端的MAC位址二是在使用ESP8266或是ESP32模組時不管程式或引用的函式庫都不一樣雖然現在大家都漸漸改用ESP32了可是如果我們只是單純要控制一個開關這時假如使用ESP32加上一個繼電器或是模組在成本上有些說不過去這時如果採用一個由ESP-01(核心晶片為ES8266)構成繼電器模組就會是一個不錯的選擇可是使用過Arduino IDE的朋友就會有個感覺就是晶片種類切換來切換去還真有些麻煩更不要說因為模組的不同所需改換的函式庫及指令函數等問題更是煩人為了讓程式更容易使用筆者花了不少時間去調整程式終於一次把前述兩個問題給解決了

以下是發送端的完整程式列表:



#ifdef ESP32

#include <WiFi.h>

  #include  <esp_now.h>

  esp_now_peer_info_t peerInfo;

  int  ledOn=1;

  int ledOff=0;

#else

  #include <ESP8266WiFi.h>

  #include  <espnow.h>

  int ledOn=0;

  int ledOff=1;

#endif

 

uint8_t macAddr[6],broadCastMacAddr[6]={0xff,0xFf,0xff,0xff,0xff,0xff};

String ESPmac="";

 

typedef struct  message{

  int ID;

  char  a[32];

  int b;

  unsigned int c;

  float d;

  bool e;

} message;

 

message myData;

 

byte  myId=1,LED=2;

unsigned int sendTimes=0;

unsigned long lastTime=0;

unsigned  long  timerDelay=5000;

 

void setup() {

 

  Serial.begin(115200);

  Serial.println();

  WiFi.macAddress(macAddr);

  for(int i=0;i<6;i++)  {

    if (macAddr[i] < 16)

      ESPmac+="0";

    ESPmac+=String(macAddr[i],HEX);

  }

  Serial.print("ESP mac Addrres = ");

  Serial.println(ESPmac);

  

//  WiFi.softAP mac Address(macAddr);

  WiFi.mode(WIFI_STA);

 

  if(esp_now_init() !=0)  {

    Serial.println("Error initializing ESP-NOW!");

    while(1)  {

      digitalWrite(LED,0);

      delay(100);

      digitalWrite(LED,1);

      delay(100);

    }

  }

#ifdef ESP32

  esp_now_register_send_cb(onDataSent);

  memcpy(peerInfo.peer_addr, broadCastMacAddr, 6);

  peerInfo.channel = 0;

  peerInfo.encrypt = false; 

  //Add peer

  if (esp_now_add_peer(&peerInfo) != ESP_OK){

    Serial.println("Failed to add peer");

    return;

  }

#else

  esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);

  esp_now_add_peer(broadCastMacAddr, ESP_NOW_ROLE_SLAVE,1, NULL, 0);

  esp_now_register_send_cb(OnDataSent);

#endif

  lastTime=millis();

}

 

void loop() {

 

  if((millis()-lastTime) > timerDelay)

  {

    lastTime=millis();

    sendTimes++;

    Serial.print("目前為第 ");Serial.print(sendTimes);Serial.println(" 筆資料,資料已發送完成!");

    myData.ID=myId;

    strcpy(myData.a,"This is a Char");

    myData.b=random(1,100);

    myData.c=sendTimes;

    myData.d=1.23;

    myData.e=true;

 

    esp_now_send(broadCastMacAddr,(uint8_t *)&myData,sizeof(myData));

  }

}

 

// ESP 使用的資料發送callback 副程式:

void  OnDataSent(uint8_t *mac_addr, uint8_t sendStatus)

{

  Serial.print("最後一筆資料傳送狀況: ");

  if (sendStatus == 0)

    Serial.println("傳送成功!\n");

  else

    Serial.println("傳送失敗!\n");

}

// ESP32 使用的資料發送callback 副程式:

#ifdef ESP32

void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {

  Serial.print("最後一筆資料傳送狀況: ");

  if (status == 0)

    Serial.println("傳送成功!\n");

  else

    Serial.println("傳送失敗!\n");

}

#endif

發送端程式列表


對於一個發送端的裝置而言必須執行下面的動作

  1. 初始化 ESP-NOW(記得要先初始化WiFi功能)

  2. 在發送數據時註冊一個回應函數 — OnDataSent』,以便在發送消息時執行此函數而此函數可以告訴我們是否成功發送資料數據

  3. 添加對應的設備(接收方)為此您需要知道接收方的 MAC 地址

  4. 向對應設備發送消息。


為了讓程式能同時使用在ESP8266與ESP32兩種模組上因此在程式一開始的引入(include)部份我們就必須先處理所使用的相關函式庫及相關變數下面的程式碼便是我們這個發送範例程式的引入部分。

由於啟動ESP-NOW的初始化功能前必須先啟動ESPxx的WiFi功能在此我們用”#ifdef … #esle … #endif”的編譯前置指令讓我們的Arduino IDE自動依所選用的ESPxx模組不同去引入不同的函式庫對ESP32而言必須用到”<WiFi.h>”和”<esp_now.h>”這兩個函式庫而對ESP8266來說則是”<ESP866WiFi.h>”及”<espnow.h>”

在我們的程式中為了確定ESP-NOW的初始化功能是否成功﹖使用了ESP8266郵票板上內建的LED(接在GPIO 2上)做為指示燈,對ESP32而言,的郵票板並沒有這顆LED,一般是作在擴充模組板(例如NodeMCU)上,而且亮滅的電壓位準和ESP8266相反,為了相容起見,我們用「ledOn」、「ledOff」這兩種個標記來代表LED的亮滅。


#ifdef ESP32

  #include <WiFi.h>

  #include  <esp_now.h>

  esp_now_peer_info_t peerInfo;

  int  ledOn=1;

  int ledOff=0;

#else

  #include <ESP8266WiFi.h>

  #include  <espnow.h>

  int ledOn=0;

  int ledOff=1;

#endif


為了避開必須先知道接收端的MAC位址才能發出資料的困擾在此我們是使用廣播(Broadcast)的方式來傳送資料也就是說發射端傳送的資料所有的接收板(假如還有其他的話)都會接收到如果接收端要知道是誰發送除了可以由發送者的MAC位址得知之外也可以在發送的資料中包含一個發送板所賦予的ID編號去辨識

下面所定義的變數中這個六位元組內容都為0xff的boardCastMacAddr便是ESP-NOW中用來標示為廣播用的MAC位址變數。


uint8_t macAddr[6],broadCastMacAddr[6]={0xff,0xFf,0xff,0xff,0xff,0xff};


為了方便展示所能傳送的資料種類,在此我們定義了一可包括各種資料型態的結構變數「message」;在其中包括了整數(int)、長整數(long)、浮點數(float)、布林數(bool)及字串(char [])等變數型態,其內容如下面程式所示。其中的「ID」用來傳送發送板所賦予的ID編號,而「c」則是代表目前發送的資料筆數流水編號。

至於後面的「myData」則是我們實際會傳送的型態為「message」的結構變數而「MyId」是這塊送發送板所賦予的ID編號變數在此為1使用者可依自己的需要改成其他的號碼


typedef struct  message{

  int ID;

  char  a[32];

  int b;

  unsigned long c;

  float d;

  bool e;

}  message;

 

message myData;

byte  myId=1,LED=2;


在前面常用指令函數介紹時說過當要初始化ESP-NOW之前必須先初始化Wi-Fi功能因此在下面屬於初始化(setup())部分的程式中我們先用『WiFi.mode(WIFI_STA)』這行指令將ESPxx晶片進行Wi-Fi功能的初始化而且設定為STA模式。然後呼叫esp_now_init()』這個ESP-NOW初始化用的指令函數如果傳回來的結果是錯誤的則會在Arduino IDE的監控視窗中顯示"Error initializing ESP-NOW!"這樣的錯誤提示訊息而且會令發送板進入一個讓接在GPIO 2的LED快速閃爍的無窮迴圈因為如果初始化失敗系統就不應該再動作下去


   WiFi.mode(WIFI_STA);

 

  if(esp_now_init() !=0)  {

    Serial.println("Error initializing ESP-NOW!");

    while(1)  {

      digitalWrite(LED,0);

      delay(100);

      digitalWrite(LED,1);

      delay(100);

    }

  }


在初始化完ESP-NOW之後接下來的動作就是註冊一個回應函數及添加對應的接收方設備的 MAC 地址由於ESP32ESP8266兩者對這些動作使用的指令及方法並不相同所以在我們的範例程式中一樣的採用”#ifdef … #esle … #endif”的編譯前置指令去把它區分出來以便可以相容使用在兩種晶片上

下面這段程式是針對ESP32而來的首先用esp_now_register_send_cb』這個指令函數去註冊一個回應函數onDataSent然後再用esp_now_add_peer』這個指令函數指定接收端的MAC位址peefInfo不過我們必須先將廣播用的MAC位址「broadCastMacAddr」複製到「peefInfo」上


#ifdef ESP32

  esp_now_register_send_cb(onDataSent);

  memcpy(peerInfo.peer_addr, broadCastMacAddr, 6);

  peerInfo.channel = 0;

  peerInfo.encrypt = false; 

  //Add peer

  if (esp_now_add_peer(&peerInfo) != ESP_OK){

    Serial.println("Failed to add peer");

    return;

  }


對於ESP8266來說必須先定義自己的腳色也就是發送端在此使用esp_now_set_self_role』這個指令函數去設定為發送者參數值為「ESP_NOW_ROLE_CONTROLLER至於指定接收端MAC位址的指令函數『esp_now_add_peer它引用參數的方式與內容跟ESP32是不一樣的這點還請讀者多注意!至於註冊一個回應函數onDataSent則是一樣使用esp_now_register_send_cb』這個指令函數


#else

  esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);

  esp_now_add_peer(broadCastMacAddr, ESP_NOW_ROLE_SLAVE,1, NULL, 0);

  esp_now_register_send_cb(OnDataSent);

#endif


下面部分則是主迴圈(loop())部分的所有程式碼內容很簡單就是在固定的時間(timerDelay🡺5秒)發送一次資料(myData)每發送一次「sendTimes」這個變數會加一用以代表資料的筆數在設定好「myData」這個資料結構變數的內容之後最後呼叫esp_now_send』這個指令函數把資料發送出去這樣便完成一次ESP-NOW的資料傳輸動作


void loop() {

  if((millis()-lastTime) > timerDelay)

  {

    lastTime=millis();

    sendTimes++;

    Serial.print("目前為第 ");Serial.print(sendTimes);Serial.println(" 筆資料,資料已發送完成!");

    myData.ID=myId;

    strcpy(myData.a,"This is a Char");

    myData.b=random(1,100);

    myData.c=sendTimes;

    myData.d=1.23;

    myData.e=true;

 

    esp_now_send(broadCastMacAddr,(uint8_t *)&myData,sizeof(myData));

  }

}


下面的程式是發送回應函數『onDataSent』的主體內容同樣的因為ESP32ESP8266兩者使用的程式指令及方法並不相同所以在此我們一樣的採用”#ifdef … #esle … #endif”的編譯前置指令去把它區分出來以便可以相容使用在兩種晶片上

在這個資料發送callback副程式中同樣的都會在Arduino IDE的監控視窗中先顯示"第 ?? 筆資料傳送狀況:"的提示訊息,其中的”??”就是「sendTimes」這個變數的內容,如果發送成功,會顯示"傳送成功!"的提示訊息,否則將顯示"傳送失敗!"的錯誤提示訊息


// ESP 使用的資料發送callback 副程式:

#ifdef ESP32

// ESP32 使用的資料發送callback 副程式:

void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {

  Serial.print("第 ");Serial.print(sendTimes);Serial.print(" 筆資料傳送狀況: ");

  if (status == 0)

    Serial.println("傳送成功!\n");

  else

    Serial.println("傳送失敗!\n");

}

#else

void  OnDataSent(uint8_t *mac_addr, uint8_t sendStatus)

{

  Serial.print("第 ");Serial.print(sendTimes);Serial.print(" 筆資料傳送狀況: ");

  if (sendStatus == 0)

    Serial.println("傳送成功!\n");

  else

    Serial.println("傳送失敗!\n");

}

#endif

執行結果:

為測試程式是否能同時相容於ESP32及ESP8266發送端程式是燒錄到ESP32上下圖為發送端裝置(即ESP32)傳送到Arduino IDE監控視窗中的提示訊息在其中會顯示所傳送資料的筆數及是否傳送成功


發送端顯示於Arduino IDE監控視窗中之訊息


◎ 接收端程式列表與說明

下列程式為接收端的完整程式列表其中前面1~28行的函式庫引入與變數定義部分和前面的發送端程式相同在此就不重複說明本範例程式和一般在網路上所見最大的不同點就是在程式中我們會令ESPxx裝置在WiFi功能中工作在AP+STA模式底下且會啟動soft-AP功能並設定一個SSID名稱而這個AP的SSID名稱中將會包含了這顆ESPxx晶片的MAC位址訊息

之所以要這麼作是為了後面延伸的應用如此一來當我們想要只針對特定ESPxx裝置傳送資料時就可以不必先把固定的接收端MAC位址用燒進的方式放進發送端的程式裡我們可以透過WiFi掃描的方式得知有那些接收裝置在我們可及的範圍內只要把它們AP的SSID名稱中的MAC位址部分萃取出來配對就可以了。

#ifdef ESP32

#include <WiFi.h>

#include  <esp_now.h>

int  ledOn = 1;

int ledOff = 0;

#else

#include <ESP8266WiFi.h>

#include  <espnow.h>

int ledOn = 0;

int ledOff = 1;

#endif

 

typedef struct message {

  int ID;

  char a[32];

  int b;

  unsigned long c;

  float d;

  bool e;

}  message;

 

// Create a struct_message called myData

message myData;

 

//uint8_t macAddr[6];

uint8_t myMacAddr[6];

String  myMacSoftSSID = "";

byte  LED = 2;

 

void setup() {

  Serial.begin(115200);    Serial.println();

  pinMode(LED, OUTPUT);

  digitalWrite(LED, 1);

  Serial.print("本機的 MAC位址為: "); Serial.println(WiFi.macAddress());

  WiFi.macAddress(myMacAddr);

  myMacSoftSSID = "ESPxx_";

  for (int i = 0; i < 6; i++)

  {

    if (myMacAddr[i] < 16)

      myMacSoftSSID += "0";

    myMacSoftSSID += String(myMacAddr[i], HEX);

  }

  Serial.print("本機的 MAC位址AP是: ");

  Serial.println(myMacSoftSSID);

 

   WiFi.mode(WIFI_AP_STA);

  char MyMacSoftSSID[30];

  myMacSoftSSID.toCharArray(MyMacSoftSSID, myMacSoftSSID.length());

  WiFi.softAP(MyMacSoftSSID);

 

  // Init ESP-NOW

  if (esp_now_init() != 0) {

    Serial.println("ESP-NOW初始化錯誤!");

    return;

  }

 

  esp_now_register_recv_cb(OnDataRecv);

}

 

void loop() {

  delay(1);

}

 

// callback function that will be executed when data is received

#ifdef ESP32

void OnDataRecv(const uint8_t * mac_addr, const uint8_t *inComingData, int len) {

#else

void OnDataRecv(uint8_t *mac_addr, uint8_t *inComingData, uint8_t len)  {

#endif

  char macStr[18];

  Serial.print("本筆資料接收自下列MAC位址: ");

  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",

           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);

  Serial.println(macStr);

  memcpy(&myData, inComingData, sizeof(myData));

  Serial.print("Bytes received: ");  Serial.println(len);

  Serial.print("Received Times: ");  Serial.println(myData.bb);

  Serial.print("Char: ");  Serial.println(myData.a);

  Serial.print("Int: ");  Serial.println(myData.b);

  Serial.print("Float: ");  Serial.println(myData.c);

  Serial.print("Bool: ");  Serial.println(myData.d);

  Serial.println();

}

接收端程式列表


在前面說過,當要初始化ESP-NOW之前必須先初始化Wi-Fi功能,在下面屬於初始化(setup())部分的程式中,我們用『WiFi.mode(WIFI_AP_STA)』這行指令將ESPxx晶片進行Wi-Fi功能的初始化,而且設定為AP+STA模式。所以要這樣設定是因為想讓外界能透過本裝置所顯示的soft-AP的SSID稱就能得知本裝置的MAC位址,也就是說如果我們用其他WiFi裝置來掃描這塊ESPxx模組時會看到”ESP_xxxxxxxxxxxx”這樣的AP名稱其中長度共12字元的’x’就是這個模組的6個bytes的MAC位址

下面的程式在把本機MAC位址轉成字串變數「myMacSoftSSID」之後也會順便在Arduino IDE監控視窗中顯示“本機的 MAC位址是: ESPxx_xxxxxxxxxxxx”的提示訊息以供使用者參考

  WiFi.macAddress(myMacAddr);

  myMacSoftSSID = "ESPxx_";

  for (int i = 0; i < 6; i++)

  {

    if (myMacAddr[i] < 16)

      myMacSoftSSID += "0";

    myMacSoftSSID += String(myMacAddr[i], HEX);

  }

  Serial.print("本機的 MAC位址是: ");

  Serial.println(myMacSoftSSID);

 

   WiFi.mode(WIFI_AP_STA);

  char MyMacSoftSSID[30];

  myMacSoftSSID.toCharArray(MyMacSoftSSID, myMacSoftSSID.length());

  WiFi.softAP(MyMacSoftSSID);


接著程式使用esp_now_init()』這個ESP-NOW初始化用的指令函數,如果傳回來的結果是錯誤的(不等於0),則會在Arduino IDE的監控視窗中顯示 ”ESP-NOW初始化錯誤!” 這樣的錯誤提示訊息


  // Init ESP-NOW

  if (esp_now_init() != 0) {

    Serial.println("ESP-NOW初始化錯誤!");

    return;

  }


下面是用esp_now_register_recv_cb這個函數指令去登錄OnDataRecv為接收服務副程式這個OnDataRecv接收服務副程式會在裝置接收到資料時被啟動使用者可以利用這個副程式執行想要做的動作例如打開或關閉一個開關。


  esp_now_register_recv_cb(OnDataRecv);


一般來說我們的主迴圈程式區可以是空的但為了避免閒置時間過久以致看門狗機制啟動所以在其中加上一個delay以便定時清除看門狗計時器。


void loop() {

  delay(1);

}


下面的程式是接收服務副程式『onDataRecv』的主體內容同樣的因為ESP32ESP8266兩者使用的程式指令及方法並不相同所以在此我們一樣的採用”#ifdef … #esle … #endif”的編譯前置指令去把它區分出來以便可以相容使用在兩種不同的晶片上。其實種兩種晶片的副程式差距很小主要是在所使用的引數宣告的方式略有不同而已至於程式主體的部分則是完全一樣。

當這個接收服務副程式因為裝置接收到數據被觸發之後會從「inComingData」這個結構變數中把對應的數據萃取出來然後顯示在Arduino IDE監控視窗中


 

// callback function that will be executed when data is received

#ifdef ESP32

void OnDataRecv(const uint8_t * mac_addr, const uint8_t *inComingData, int len) {

#else

void OnDataRecv(uint8_t *mac_addr, uint8_t *inComingData, uint8_t len)  {

#endif

  char macStr[18];

  Serial.print("本筆資料接收自下列MAC位址: ");

  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",

           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);

  Serial.println(macStr);

  memcpy(&myData, inComingData, sizeof(myData));

  Serial.print("Bytes received: ");  Serial.println(len);

  Serial.print("Received Times: ");  Serial.println(myData.bb);

  Serial.print("Char: ");  Serial.println(myData.a);

  Serial.print("Int: ");  Serial.println(myData.b);

  Serial.print("Float: ");  Serial.println(myData.c);

  Serial.print("Bool: ");  Serial.println(myData.d);

  Serial.println();

}


執行結果:

下面的圖案是接收端於Arduino IDE監控視窗中顯示從發送端所接收到的訊息畫面其中標記1是發送裝置所賦予的ID號碼而標記2而是這筆資料的流水編號每次會加1至於後面的內容可依使用者的需求自行安排設定例如設定某一支I/O腳的輸出狀態以用來控制某個開關或繼電器的動作


接收端於Arduino IDE監控視窗中顯示之訊息


沒有留言:

張貼留言