作者:黃東正
繼上文介紹過『一對一單向架構』後,本文中將繼續介紹『雙向多對多傳輸』這個ESP-NOW中最複雜的拓樸結構的通信方式。
照理來說應該循序漸進從簡至繁陸續介紹其他各種ESP-NOW拓樸結構的通信方式,不過在設計『 一對一雙向傳輸』這種拓樸架構的程式時發現,由於我們的發送端是以廣播的方式發送資料,也就是說只要是在電波信號可及的場域中,所有的ESPxx裝置都可以收到這些數據,這麼一來就等同於建構了『雙向多對多傳輸』這個拓樸結構的通信方式,既然這樣就乾脆一次到位,直接為大家介紹這種拓樸結構就好了。
發送端程式列表與說明
在這個範例中會示範多片的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熄滅。
下列程式即為此雙向多對多收發程式的列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | #ifdef ESP32 #include #include esp_now_peer_info_t peerInfo; int LedOn=1; int LedOff=0; String ESPtype="ESP32_"; #else #include #include 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」這個新字串變數的用途。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #ifdef ESP32 #include #include esp_now_peer_info_t peerInfo; int LedOn=1; int LedOff=0; String ESPtype="ESP32_"; #else #include #include int LedOn=0; int LedOff=1; String ESPtype="ESP8266_"; #endif |
為了避開必須先知道接收端的MAC位址才能發出資料的困擾,在此我們是使用廣播(Broadcast)的方式來傳送資料,也就是說發射端傳送的資料所有其他的模組板都會接收到,如果接收端要知道是誰發送,除了可以由發送者的MAC位址得知之外,也可以在發送的資料中包含一個發送板所賦予的ID編號去辨識。
下面所定義的變數中,這個六位元組內容都為0xff的「boardCastMacAddr」,便是ESP-NOW中用來標示為廣播用的MAC位址變數。
1 | 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編號的訊息便可以得知發送者是哪一塊模組板了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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名稱。
1 2 3 | 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位址裝置傳送數據之用。
1 | // 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初始化錯誤!
1 | if(esp_now_init() !=0) { Serial.println("ESP-NOW!初始化失敗"); while(1) { digitalWrite(LED,0); delay(100); digitalWrite(LED,1); delay(100); } } |
在初始化完ESP-NOW之後,接下來的動作就是註冊一些回應函數,及添加對應的接收方設備的 MAC 地址,由於ESP32、ESP8266兩者對這些動作使用的指令及方法並不相同,所以在我們的範例程式中一樣的採用”#ifdef … #esle … #endif”的編譯前置指令去把它區分出來,以便可以相容使用在兩種晶片上。
對ESP32而來的,並不用先特別設定他的角色是發送端還是接收端,首先用『esp_now_register_send_cb』這個指令函數去註冊一個發送的回應函數「onDataSent」,然後再用『esp_now_add_peer』這個指令函數指定接收端的MAC位址「peefInfo」,不過我們必須先將廣播用的MAC位址「broadCastMacAddr」共6個bytes複製到「peefInfo」上,這部分跟上一節發送端部分的程式內容式相同的。
1 | #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』為接收服務副程式。
1 | #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)』這行指令,讓該布林數值反相,以達到不斷交互改變的結果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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』的主體內容,同樣的因為ESP32、ESP8266兩者使用的程式指令及方法並不相同,所以在此我們一樣的採用”#ifdef … #esle … #endif”的編譯前置指令去把它區分出來,以便可以相容使用在兩種晶片上。其實兩者的主體程式部分內容是相同的,只有在呼叫時所配置的引數部分略有不同(第三個),但也就是這點小差異造成兩種晶片無法共用同一個副程式。
在這個資料發送callback副程式中,同樣的都會在Arduino IDE的監控視窗中先顯示”第 ?? 筆資料傳送狀況:”的提示訊息,其中的”??”就是「sendTimes」這個變數的內容,如果發送成功,會顯示”傳送成功!”的提示訊息,否則將顯示”傳送失敗!”的錯誤提示訊息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 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』的主體內容,同樣的因為ESP32、ESP8266兩者使用的程式指令及方法並不相同,所以在此我們一樣的採用”#ifdef … #esle … #endif”的編譯前置指令去把它區分出來,以便可以相容使用在兩種不同的晶片上。其實種兩種晶片的副程式差距很小,主要是在所使用的引數宣告的方式略有不同而已,至於程式主體的部分則是完全一樣。
當這個接收服務副程式因為裝置接收到數據被觸發之後,會從「inComingData」這個結構變數中把對應的數據萃取出來,然後顯示在Arduino IDE監控視窗中,並且把提示訊息都改用中文。這個結構變數的內容和上一節的範例大同小異,主要有兩個數據的功能不一樣,一是字元陣列「myData.a」也就是字串變數部分,顯示的是發送端晶片的種類及其MAC位址碼,再來就是布林數「myData.d」會被用來決定接在GPIO 2上的LED是點亮還是熄滅,並且會將結果顯示在Arduino IDE監控視窗上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // 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點亮” (標記3、4)。
至於下面是本範例系統中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位址單獨針對特定的發送端回應接收情形或處理的結果。
(作者為本刊專欄作家,本文同步表於作者部落格,原文連結;責任編輯:謝嘉洵)
- 認識ESP-NOW協定 Part 4:雙向多對多架構 - 2023/01/09
- 認識ESP-NOW協定 Part 3:一對一單向架構 - 2023/01/09
- 認識ESP-NOW協定 Part 2:拓樸架構介紹 - 2022/12/01
訂閱MakerPRO知識充電報
與40000位開發者一同掌握科技創新的技術資訊!