2022年11月6日 星期日

三、2雙向多對多架構


在這個小節中將示範『二、6雙向多對多傳輸』這個ESP-NOW中最複雜的拓樸結構的通信方式,照理來說應該循序漸進從簡至繁陸續介紹其他各種ESP-NOW拓樸結構的通信方式,不過在設計『二、2 一對一雙向傳輸』這種拓樸架構的程式時發現,由於我們的發送端是以廣播的方式發送資料,也就是說只要是在電波信號可及的場域中,所有的ESPxx裝置都可以收到這些數據,這麼一來就等同於建構了『二、6雙向多對多傳輸』這個拓樸結構的通信方式,既然這樣就乾脆一次到位,直接為大家介紹這種拓樸結構就好了


雙向多對多傳輸


◎ 發送端程式列表與說明

在這個範例中會示範多片的ESPxx(可混合使用ESP8266與ESP32兩種晶片)模組板互相通信傳輸資料的方法其主要特點如下

  • 本範例程式可同時給ESP8266與ESP32兩種不同晶片的模組板使用不必做任何的修改

  • 本範例程式可同時使用在發送與接收端的模組板也就是說一個程式即可涵蓋所有的拓樸模式與主從角色當然也不用再做任何的修改

  • 在這個範例中,我們將有兩塊 ESP8266 和一塊ESP32模組板作為整個系統的成員每塊板ESPxx模組板都具有雙向通信的功能而且它們發送資料的方式是以廣播的方為之也就是其它的ESPxx模組板都會接收到任一發送裝置發送出去的資料

  • 每塊板ESPxx模組板都具有WiFi AP的腳色它們的SSID名稱為其晶片的種類加上他的MAC位址例如ESP8266晶片它的AP SSID名稱為ESP8266_xxxxxxxxxxxx其中的12個’x’便是6個bytes的MAC位址值當使用者用手機去掃描這些ESPxx模組的WiFi AP點時,便可由它們的SSID名稱得知其MAC位址

  • 在系統中所發送的範例資料和上一節一樣為一結構變數其中包括了不同種類的變數型態而其中的字串變數將改成該模組的SSID名稱以作為辨識發送者的ID名稱之用。

  • 發送的範例資料結構變數中的布林變數可用來控制ESPxx模組板上接在GPIO 2上的LED亮滅之用若值為true則會點亮LED反之如果是false則會令LED熄滅。


下列程式即為此雙向多對多收發程式的列表

#ifdef ESP32

  #include <WiFi.h>

  #include  <esp_now.h>

  esp_now_peer_info_t peerInfo;

  int  LedOn=1;

  int LedOff=0;

  String  ESPtype="ESP32_";

#else

  #include <ESP8266WiFi.h>

  #include  <espnow.h>

  int LedOn=0;

  int LedOff=1;

  String  ESPtype="ESP8266_";

#endif

 

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

 

String ESPmac="";

 

typedef struct  message{

  char  a[32];

  int b;

  unsigned long bb;

  float c;

  bool  d;

} message;

 

message myData;

String  myMacSoftSSID="";

byte  LED=2;

bool  LedStatus=false;

unsigned int sendTimes=0;

 

unsigned long lastTime=0;

unsigned  long  timerDelay=10000;

 

void setup() {

 

  Serial.begin(115200);

  Serial.println();

  pinMode(LED,OUTPUT);

  WiFi.macAddress(macAddr);

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

    if (macAddr[i] < 16)

      ESPmac+="0";

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

  }

  myMacSoftSSID=ESPtype+ESPmac;

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

  Serial.println(ESPmac);

  

//  WiFi.softAP mac Address(macAddr);

  char MyMacSoftSSID[30];

  WiFi.mode(WIFI_AP_STA);

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

  WiFi.softAP(MyMacSoftSSID);

 

//  Serial.print("WiFi 的 channel 號碼為: "); Serial.println(getWiFiChannel(ssid));

  if(esp_now_init() !=0)  {

    Serial.println("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_COMBO);

  esp_now_register_send_cb(OnDataSent);

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

#endif

 

  esp_now_register_recv_cb(OnDataRecv);

  lastTime=millis();

}

 

void loop() {

 

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

  {

    lastTime=millis();

    Serial.println("資料已發送完成!");

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

    myMacSoftSSID.toCharArray(myData.a,myMacSoftSSID.length()+1);

    sendTimes++;

    myData.bb=sendTimes;

    myData.b=random(1,100);

    myData.c=1.23;

    LedStatus=!(LedStatus);

    myData.d=LedStatus;

//    Serial.println(myData.d);

 

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

  }

}

 

// 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

 

// 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];

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

  Serial.print("接收到的位元組(bytes): ");  Serial.println(len);

  Serial.print("接收資料來源: ");  Serial.println(myData.a);

  Serial.print("接收的筆數: ");  Serial.println(myData.bb);

  Serial.print("整數: ");  Serial.println(myData.b);

  Serial.print("浮點數: ");  Serial.println(myData.c);

  Serial.print("布林數: ");  Serial.println(myData.d);

  if (myData.d == true)

  {

    Serial.println("LED點亮!"); 

    digitalWrite(LED,LedOn);

  }

  else  {

    Serial.println("LED熄滅!");

    digitalWrite(LED,LedOff);

  }

  Serial.println();

}

 

int32_t getWiFiChannel(const char *ssid) {

  if (int32_t n = WiFi.scanNetworks()) {

    for (uint8_t i=0; i<n; i++) {

      if (!strcmp(ssid, WiFi.SSID(i).c_str())) {

        return WiFi.channel(i);

      }

    }

  }

  return 0;

}

雙向多對多收發程式列表


下列程式為雙向多對多收發程式前面的函式庫引入與變數定義部分,為了讓程式能同時使用在ESP8266與ESP32兩種模組上因此在程式一開始的引入(include)部份我們就必須先處理所使用的相關函式庫及相關變數基本上和前一節的範例大致相同。

前面說過本範例程式和一般在網路上所見最大的不同點,就是在程式中我們會令ESPxx裝置在WiFi功能中工作在AP+STA模式底下,且會啟動soft-AP功能,並設定一個SSID名稱,而這個AP的SSID名稱中將會包含了這顆ESPxx晶片的MAC位址訊息並且是以該晶片種類的編號開頭這就是ESPtype這個新字串變數的用途。


#ifdef ESP32

  #include <WiFi.h>

  #include  <esp_now.h>

  esp_now_peer_info_t peerInfo;

  int  LedOn=1;

  int LedOff=0;

  String  ESPtype="ESP32_";

#else

  #include <ESP8266WiFi.h>

  #include  <espnow.h>

  int LedOn=0;

  int LedOff=1;

  String  ESPtype="ESP8266_";

#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 [])等變數型態,其內容如下面程式所示。至於後面的「myData」則是我們實際會傳送的型態為「message」的結構變數在前一節的範例中我們用了一個整數作為送發送板的ID編號變數不過這個ID編號還是必須在燒錄程式時給定但這樣一來程式就太沒有彈性了在此為了能動態的自行配置這個ID我們的程式將會自動把前面提過的包含了這顆ESPxx晶片中類和MAC位址訊息的AP SSID名稱字串變數指定給字串變數「char []」作為發送端的ID,這樣接收端從ID編號的訊息便可以得知發送者是哪一塊模組板了。


typedef struct  message{

  char  a[32];

  int b;

  unsigned long bb;

  float c;

  bool  d;

} message;

 

message myData;

String  myMacSoftSSID="";

byte  LED=2;

bool  LedStatus=false;

unsigned int sendTimes=0;

 

unsigned long lastTime=0;

unsigned  long  timerDelay=10000;


至於下面的程式會先取得這顆ESPxx晶片的MAC位址訊息並且與包含該晶片種類編號開頭的變數ESPtype組合後得到myMacSoftSSID這個字串變數以作為模組板的AP SSID名稱 


WiFi.macAddress(macAddr);

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

    if (macAddr[i] < 16)

      ESPmac+="0";

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

  }

  myMacSoftSSID=ESPtype+ESPmac;

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

  Serial.println(ESPmac);


在前面常說過要初始化ESP-NOW之前必須先初始化Wi-Fi功能因此在下面的程式中我們先用『WiFi.mode(WIFI_AP_STA)』這行指令將ESPxx晶片進行Wi-Fi功能的初始化而且設定為AP+STA模式。接著使用WiFi.softAP(MyMacSoftSSID)這個指令函數建立一個AP存取點,其名稱為myMacSoftSSID」,以便外界可以得知這塊模組板的MAC位址值,好作為對特定MAC位址裝置傳送數據之用


 //  WiFi.softAP mac Address(macAddr);

  char MyMacSoftSSID[30];

  WiFi.mode(WIFI_AP_STA);

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

  WiFi.softAP(MyMacSoftSSID);

 


然後呼叫esp_now_init()』這個ESP-NOW初始化用的指令函數如果傳回來的結果是錯誤的則會在Arduino IDE的監控視窗中顯示" ESP-NOW!初始化失敗!"這樣的錯誤提示訊息而且會令ESPxx模組板上接在GPIO 2的LED進入一個快速閃爍的無窮迴圈以提醒使用者ESP-NOW初始化錯誤


  if(esp_now_init() !=0)  {

    Serial.println("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」共6個bytes複製到「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』這個指令函數

對於ESP32和ESP8266來說對於接收部分的設定是相同的因此共用了下面esp_now_register_recv_cb這個函數指令去登錄OnDataRecv為接收服務副程式。


#else

  esp_now_set_self_role(ESP_NOW_ROLE_COMBO);

  esp_now_register_send_cb(OnDataSent);

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

#endif

 

  esp_now_register_recv_cb(OnDataRecv);

  lastTime=millis();

}


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

「myData」這個資料結構變數的內容和上一節最大的不同之處就是字串變數「char []」的內容改設定為包含了這顆ESPxx晶片種類及MAC位址訊息的AP SSID名稱變數「myMacSoftSSID作為發送端的ID在此使用myMacSoftSSID.toCharArray這個延伸字串指令把字串變數「myMacSoftSSID」轉換成「myData.a這個字元陣列變數以免變數型態不合編譯不過!

除此之外我們還把其中的布林數,也就是「myData.d」在接收端用來控制接在GPIO 2的LED亮滅當接收值為”true”會點亮LED反之則令LED熄滅。為了能讓LED能產生亮滅的效果,我們必須不斷改變這個布林數,在此每當發送事件啟動也就是10秒到了會執行LedStatus=!(LedStatus)這行指令,讓該布林數值反相,以達到不斷交互改變的結果。


void loop() {

 

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

  {

    lastTime=millis();

    Serial.println("資料已發送完成!");

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

    myMacSoftSSID.toCharArray(myData.a,myMacSoftSSID.length()+1);

    sendTimes++;

    myData.bb=sendTimes;

    myData.b=random(1,100);

    myData.c=1.23;

    LedStatus=!(LedStatus);

    myData.d=LedStatus;

//    Serial.println(myData.d);

 

    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


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

當這個接收服務副程式因為裝置接收到數據被觸發之後會從「inComingData」這個結構變數中把對應的數據萃取出來然後顯示在Arduino IDE監控視窗中並且把提示訊息都改用中文這個結構變數的內容和上一節的範例大同小異,主要有兩個數據的功能不一樣,一是字元陣列「myData.a」也就是字串變數部分,顯示的是發送端晶片的種類及其MAC位址碼,再來就是布林數「myData.d」會被用來決定接在GPIO 2上的LED是點亮還是熄滅並且會將結果顯示在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];

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

  Serial.print("接收到的位元組(bytes): ");  Serial.println(len);

  Serial.print("接收資料來源: ");  Serial.println(myData.a);

  Serial.print("接收的筆數: ");  Serial.println(myData.bb);

  Serial.print("整數: ");  Serial.println(myData.b);

  Serial.print("浮點數: ");  Serial.println(myData.c);

  Serial.print("布林數: ");  Serial.println(myData.d);

  if (myData.d == true)

  {

    Serial.println("LED點亮!"); 

    digitalWrite(LED,LedOn);

  }

  else  {

    Serial.println("LED熄滅!");

    digitalWrite(LED,LedOff);

  }

  Serial.println();

}


執行結果:

當我們把本範例系統配置的三片EXPxx模組板(兩片ESP8266加一片ESP32)接電啟動之後,開啟手機Wi-Fi掃描功能,便可看到類似下面圖片畫面的訊息;其中標記1、2是代表兩片ESP8266模組板的MAC位址數值的AP存取點SSID名稱,而標記3則是ESP32模組板的訊息。



至於在Arduino IDE監控視窗上,對不同的ESPxx模組板來說會看到不同接收訊息,以下圖來說就是本次範例系統中ESP32所看到內容,除了他本身發送資料的訊息(標記5)之外共有兩筆資料,分別是來自MAC位址為「a0:20:a6:14:41:22」(標記1)與「2c:f4:32:17:83:50」(標記2)這兩塊ESP8266模組板的接收資料;而且當接收到的布林數為’0’時會出現”LED熄滅”的訊息 (標記3) 若布林數為’1’時的訊息改為”LED點亮” (標記4)。


下面是本範例系統中MAC位址為「a0:20:a6:14:41:22」這塊ESP8266模組板Arduino IDE監控視窗上所看到的畫面內容,除了他本身發送資料的訊息(標記5)之外共有兩筆資料,分別是來自ESP32 MAC位址為「24:6f:28:b1:77:28」(標記1)與「2c:f4:32:17:83:50」(標記2)這塊ESP8266模組板的接收資料而且當接收到的布林數為’1’時的訊息為”LED點亮” (標記34)。



至於下面是本範例系統中MAC位址為「2c:f4:32:17:83:50」這塊ESP8266模組板Arduino IDE監控視窗上所看到的畫面內容,除了他本身發送資料的訊息(標記5)之外同樣有兩筆資料,分別是來自MAC位址為「a0:20:a6:14:41:22」(標記1)這塊ESP8266模組板和MAC位址為「24:6f:28:b1:77:28」(標記2)的 ESP32模組板而且同樣的當接收到的布林數為’1’時會出現” LED點亮”的訊息 (標記3) 如果布林數為’0’時的訊息改為” LED熄滅” (標記4)。



經由前面說明讀者應該可以發現雖然說本小節中所示範的『二、6雙向多對多傳輸』這個拓樸結構的通信方式是ESP-NOW中最複雜的可是所示範的程式反而是比較簡單或者說單純因為只有一個收發兩用程式而已不像前一節的範例還必須分別去設計一個發送及接收的程式此外在接收的資料中我們還示範如何傳送該ESPxx模組板的MAX位址這樣接收到資料的裝置也可以利用該MAC位址單獨針對特定的發送端回應接收情形或處理的結果


沒有留言:

張貼留言