From d01a89eec6a76b07f9320baf5da22cae376b3627 Mon Sep 17 00:00:00 2001 From: Khoi Hoang <57012152+khoih-prog@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:09:47 -0500 Subject: [PATCH] v2.0.0 for `ESP32 + LwIP W6100` ### Initial Releases v2.0.0 1. Initial coding to port [AsyncUDP](https://github.com/espressif/arduino-esp32/tree/master/libraries/AsyncUDP) to ESP32 boards using `LwIP W6100 Ethernet` 2. Add more examples. 3. Add debugging features. 4. Bump up to v2.0.0 to sync with [AsyncUDP v2.0.0](https://github.com/espressif/arduino-esp32/tree/master/libraries/AsyncUDP). --- .codespellrc | 7 + CONTRIBUTING.md | 82 + LibraryPatches/esp32/cores/esp32/Server.h | 35 + changelog.md | 33 + examples/AsyncUDPClient/AsyncUDPClient.ino | 175 ++ .../AsyncUDPMulticastServer.ino | 173 ++ examples/AsyncUDPServer/AsyncUDPServer.ino | 164 ++ .../AsyncUdpNTPClient/AsyncUdpNTPClient.ino | 244 +++ .../AsyncUdpSendReceive.ino | 262 +++ .../multiFileProject/multiFileProject.cpp | 12 + examples/multiFileProject/multiFileProject.h | 17 + .../multiFileProject/multiFileProject.ino | 73 + keywords.txt | 70 + library.json | 46 + library.properties | 13 + pics/W6100.png | Bin 0 -> 118672 bytes platformio/platformio.ini | 155 ++ src/AsyncUDP_ESP32_W6100.h | 72 + src/AsyncUDP_ESP32_W6100.hpp | 183 +++ src/AsyncUDP_ESP32_W6100_Debug.h | 93 ++ src/AsyncUDP_ESP32_W6100_Impl.h | 1443 +++++++++++++++++ utils/astyle_library.conf | 70 + utils/restyle.sh | 6 + 23 files changed, 3428 insertions(+) create mode 100644 .codespellrc create mode 100644 CONTRIBUTING.md create mode 100644 LibraryPatches/esp32/cores/esp32/Server.h create mode 100644 changelog.md create mode 100644 examples/AsyncUDPClient/AsyncUDPClient.ino create mode 100644 examples/AsyncUDPMulticastServer/AsyncUDPMulticastServer.ino create mode 100644 examples/AsyncUDPServer/AsyncUDPServer.ino create mode 100644 examples/AsyncUdpNTPClient/AsyncUdpNTPClient.ino create mode 100644 examples/AsyncUdpSendReceive/AsyncUdpSendReceive.ino create mode 100644 examples/multiFileProject/multiFileProject.cpp create mode 100644 examples/multiFileProject/multiFileProject.h create mode 100644 examples/multiFileProject/multiFileProject.ino create mode 100644 keywords.txt create mode 100644 library.json create mode 100644 library.properties create mode 100644 pics/W6100.png create mode 100644 platformio/platformio.ini create mode 100644 src/AsyncUDP_ESP32_W6100.h create mode 100644 src/AsyncUDP_ESP32_W6100.hpp create mode 100644 src/AsyncUDP_ESP32_W6100_Debug.h create mode 100644 src/AsyncUDP_ESP32_W6100_Impl.h create mode 100644 utils/astyle_library.conf create mode 100644 utils/restyle.sh diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..00fe362 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,7 @@ +# See: https://github.com/codespell-project/codespell#using-a-config-file +[codespell] +# In the event of a false positive, add the problematic word, in all lowercase, to a comma-separated list here: +ignore-words-list = , +check-filenames = +check-hidden = +skip = ./.git,./src,./examples,./Packages_Patches,./LibraryPatches diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..72ca147 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,82 @@ +## Contributing to AsyncUDP_ESP32_W6100 + +### Reporting Bugs + +Please report bugs in AsyncUDP_ESP32_W6100 if you find them. + +However, before reporting a bug please check through the following: + +* [Existing Open Issues](https://github.com/khoih-prog/AsyncUDP_ESP32_W6100/issues) - someone might have already encountered this. + +If you don't find anything, please [open a new issue](https://github.com/khoih-prog/AsyncUDP_ESP32_W6100/issues/new). + +### How to submit a bug report + +Please ensure to specify the following: + +* Arduino IDE version (e.g. 1.8.19) or Platform.io version +* Board Core Version (e.g. ESP32 core v2.0.6) +* Contextual information (e.g. what you were trying to achieve) +* Simplest possible steps to reproduce +* Anything that might be relevant in your opinion, such as: + * Operating system (Windows, Ubuntu, etc.) and the output of `uname -a` + * Network configuration + + +Please be educated, civilized and constructive as you've always been. Disrespective posts against [GitHub Code of Conduct](https://docs.github.com/en/site-policy/github-terms/github-event-code-of-conduct) will be ignored and deleted. + +--- + +### Example + +``` +Arduino IDE version: 1.8.19 +ESP32_DEV board +ESP32 core v2.0.6 +OS: Ubuntu 20.04 LTS +Linux xy-Inspiron-3593 5.15.0-57-generic #63~20.04.1-Ubuntu SMP Wed Nov 30 13:40:16 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux + +Context: +I encountered a crash while using this library +Steps to reproduce: +1. ... +2. ... +3. ... +4. ... +``` + +--- + +### Additional context + +Add any other context about the problem here. + +--- + +### Sending Feature Requests + +Feel free to post feature requests. It's helpful if you can explain exactly why the feature would be useful. + +There are usually some outstanding feature requests in the [existing issues list](https://github.com/khoih-prog/AsyncUDP_ESP32_W6100/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement), feel free to add comments to them. + +--- + +### Sending Pull Requests + +Pull Requests with changes and fixes are also welcome! + +Please use the `astyle` to reformat the updated library code as follows (demo for Ubuntu Linux) + +1. Change directory to the library GitHub + +``` +xy@xy-Inspiron-3593:~$ cd Arduino/xy/AsyncUDP_ESP32_W6100_GitHub/ +xy@xy-Inspiron-3593:~/Arduino/xy/AsyncUDP_ESP32_W6100_GitHub$ +``` + +2. Issue astyle command + +``` +xy@xy-Inspiron-3593:~/Arduino/xy/AsyncUDP_ESP32_W6100_GitHub$ bash utils/restyle.sh +``` + diff --git a/LibraryPatches/esp32/cores/esp32/Server.h b/LibraryPatches/esp32/cores/esp32/Server.h new file mode 100644 index 0000000..2a677df --- /dev/null +++ b/LibraryPatches/esp32/cores/esp32/Server.h @@ -0,0 +1,35 @@ +/* + Server.h - Base class that provides Server + Copyright (c) 2011 Adrian McEwen. All right reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef server_h +#define server_h + +#include "Print.h" + +class Server: public Print +{ + public: + // KH, change to fix compiler error for EthernetWebServer + // error: cannot declare field 'EthernetWebServer::_server' to be of abstract type 'EthernetServer' + // virtual void begin(uint16_t port=0) =0; + //virtual void begin() = 0; + void begin() {}; +}; + +#endif diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..da03e8c --- /dev/null +++ b/changelog.md @@ -0,0 +1,33 @@ +# AsyncUDP_ESP32_W6100 + + +[![arduino-library-badge](https://www.ardu-badge.com/badge/AsyncUDP_ESP32_W6100.svg?)](https://www.ardu-badge.com/AsyncUDP_ESP32_W6100) +[![GitHub release](https://img.shields.io/github/release/khoih-prog/AsyncUDP_ESP32_W6100.svg)](https://github.com/khoih-prog/AsyncUDP_ESP32_W6100/releases) +[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](#Contributing) +[![GitHub issues](https://img.shields.io/github/issues/khoih-prog/AsyncUDP_ESP32_W6100.svg)](http://github.com/khoih-prog/AsyncUDP_ESP32_W6100/issues) + + +Donate to my libraries using BuyMeACoffee + + +--- +--- + +## Table of Contents + +* [Changelog](#changelog) + * [Initial Releases v2.0.0](#initial-releases-v200) + +--- +--- + +## Changelog + +### Initial Releases v2.0.0 + +1. Initial coding to port [AsyncUDP](https://github.com/espressif/arduino-esp32/tree/master/libraries/AsyncUDP) to ESP32 boards using `LwIP W6100 Ethernet` +2. Add more examples. +3. Add debugging features. +4. Bump up to v2.0.0 to sync with [AsyncUDP v2.0.0](https://github.com/espressif/arduino-esp32/tree/master/libraries/AsyncUDP). + + diff --git a/examples/AsyncUDPClient/AsyncUDPClient.ino b/examples/AsyncUDPClient/AsyncUDPClient.ino new file mode 100644 index 0000000..4f91fab --- /dev/null +++ b/examples/AsyncUDPClient/AsyncUDPClient.ino @@ -0,0 +1,175 @@ +/**************************************************************************************************************************** + Async_UdpClient.ino + + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license + *****************************************************************************************************************************/ + +#if !( defined(ESP32) ) + #error This code is designed for (ESP32 + W6100) to run on ESP32 platform! Please check your Tools->Board setting. +#endif + +#include + +#define ASYNC_UDP_ESP32_W6100_DEBUG_PORT Serial + +// Use from 0 to 4. Higher number, more debugging messages and memory usage. +#define _ASYNC_UDP_ESP32_W6100_LOGLEVEL_ 1 + +////////////////////////////////////////////////////////// + +// Optional values to override default settings +// Don't change unless you know what you're doing +//#define ETH_SPI_HOST SPI3_HOST +//#define SPI_CLOCK_MHZ 25 + +// Must connect INT to GPIOxx or not working +//#define INT_GPIO 4 + +//#define MISO_GPIO 19 +//#define MOSI_GPIO 23 +//#define SCK_GPIO 18 +//#define CS_GPIO 5 + +////////////////////////////////////////////////////////// + +#include + +IPAddress remoteIPAddress = IPAddress(192, 168, 2, 112); + +#define UDP_REMOTE_PORT 5698 + +///////////////////////////////////////////// + +// Enter a MAC address and IP address for your controller below. +#define NUMBER_OF_MAC 20 + +byte mac[][NUMBER_OF_MAC] = +{ + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x01 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x02 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x03 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x04 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x05 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x06 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x07 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x08 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x09 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0A }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0B }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0C }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0D }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0E }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0F }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x10 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x11 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x12 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x13 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x14 }, +}; + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +///////////////////////////////////////////// + +AsyncUDP udp; + +void parsePacket(AsyncUDPPacket packet) +{ + Serial.print("UDP Packet Type: "); + Serial.print(packet.isBroadcast() ? "Broadcast" : packet.isMulticast() ? "Multicast" : "Unicast"); + Serial.print(", From: "); + Serial.print(packet.remoteIP()); + Serial.print(":"); + Serial.print(packet.remotePort()); + Serial.print(", To: "); + Serial.print(packet.localIP()); + Serial.print(":"); + Serial.print(packet.localPort()); + Serial.print(", Length: "); + Serial.print(packet.length()); + Serial.print(", Data: "); + Serial.write(packet.data(), packet.length()); + Serial.println(); + //reply to the client + packet.printf("Got %u bytes of data", packet.length()); +} + +void setup() +{ + Serial.begin(115200); + + while (!Serial && (millis() < 5000)); + + Serial.print(F("\nStart Async_UDPClient on ")); + Serial.print(ARDUINO_BOARD); + Serial.print(F(" with ")); + Serial.println(SHIELD_TYPE); + Serial.println(WEBSERVER_ESP32_W6100_VERSION); + Serial.println(ASYNC_UDP_ESP32_W6100_VERSION); + + Serial.setDebugOutput(true); + + UDP_LOGWARN(F("Default SPI pinout:")); + UDP_LOGWARN1(F("SPI_HOST:"), ETH_SPI_HOST); + UDP_LOGWARN1(F("MOSI:"), MOSI_GPIO); + UDP_LOGWARN1(F("MISO:"), MISO_GPIO); + UDP_LOGWARN1(F("SCK:"), SCK_GPIO); + UDP_LOGWARN1(F("CS:"), CS_GPIO); + UDP_LOGWARN1(F("INT:"), INT_GPIO); + UDP_LOGWARN1(F("SPI Clock (MHz):"), SPI_CLOCK_MHZ); + UDP_LOGWARN(F("=========================")); + + /////////////////////////////////// + + // To be called before ETH.begin() + ESP32_W6100_onEvent(); + + // start the ethernet connection and the server: + // Use DHCP dynamic IP and random mac + //bool begin(int MISO_GPIO, int MOSI_GPIO, int SCLK_GPIO, int CS_GPIO, int INT_GPIO, int SPI_CLOCK_MHZ, + // int SPI_HOST, uint8_t *W6100_Mac = W6100_Default_Mac); + ETH.begin( MISO_GPIO, MOSI_GPIO, SCK_GPIO, CS_GPIO, INT_GPIO, SPI_CLOCK_MHZ, ETH_SPI_HOST ); + //ETH.begin( MISO_GPIO, MOSI_GPIO, SCK_GPIO, CS_GPIO, INT_GPIO, SPI_CLOCK_MHZ, ETH_SPI_HOST, mac[millis() % NUMBER_OF_MAC] ); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + //ETH.config(myIP, myGW, mySN, myDNS); + + ESP32_W6100_waitForConnect(); + + /////////////////////////////////// + + // Client address + Serial.print("Async_UDPClient started @ IP address: "); + Serial.println(ETH.localIP()); + + if (udp.connect(remoteIPAddress, UDP_REMOTE_PORT)) + { + Serial.println("UDP connected"); + + udp.onPacket([](AsyncUDPPacket packet) + { + parsePacket( packet); + }); + + //Send unicast + udp.print("Hello Server!"); + } +} + +void loop() +{ + delay(10000); + //Send broadcast on port UDP_REMOTE_PORT = 1234 + udp.broadcastTo("Anyone here?", UDP_REMOTE_PORT); +} diff --git a/examples/AsyncUDPMulticastServer/AsyncUDPMulticastServer.ino b/examples/AsyncUDPMulticastServer/AsyncUDPMulticastServer.ino new file mode 100644 index 0000000..d0e8221 --- /dev/null +++ b/examples/AsyncUDPMulticastServer/AsyncUDPMulticastServer.ino @@ -0,0 +1,173 @@ +/**************************************************************************************************************************** + AsyncUDPMulticastServer.ino + + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license + *****************************************************************************************************************************/ + +#if !( defined(ESP32) ) + #error This code is designed for (ESP32 + W6100) to run on ESP32 platform! Please check your Tools->Board setting. +#endif + +#include + +#define ASYNC_UDP_ESP32_W6100_DEBUG_PORT Serial + +// Use from 0 to 4. Higher number, more debugging messages and memory usage. +#define _ASYNC_UDP_ESP32_W6100_LOGLEVEL_ 1 + +////////////////////////////////////////////////////////// + +// Optional values to override default settings +// Don't change unless you know what you're doing +//#define ETH_SPI_HOST SPI3_HOST +//#define SPI_CLOCK_MHZ 25 + +// Must connect INT to GPIOxx or not working +//#define INT_GPIO 4 + +//#define MISO_GPIO 19 +//#define MOSI_GPIO 23 +//#define SCK_GPIO 18 +//#define CS_GPIO 5 + +////////////////////////////////////////////////////////// + +#include + +///////////////////////////////////////////// + +// Enter a MAC address and IP address for your controller below. +#define NUMBER_OF_MAC 20 + +byte mac[][NUMBER_OF_MAC] = +{ + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x01 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x02 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x03 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x04 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x05 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x06 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x07 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x08 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x09 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0A }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0B }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0C }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0D }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0E }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0F }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x10 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x11 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x12 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x13 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x14 }, +}; + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +///////////////////////////////////////////// + +AsyncUDP udp; + +void parsePacket(AsyncUDPPacket packet) +{ + Serial.print("UDP Packet Type: "); + Serial.print(packet.isBroadcast() ? "Broadcast" : packet.isMulticast() ? "Multicast" : "Unicast"); + Serial.print(", From: "); + Serial.print(packet.remoteIP()); + Serial.print(":"); + Serial.print(packet.remotePort()); + Serial.print(", To: "); + Serial.print(packet.localIP()); + Serial.print(":"); + Serial.print(packet.localPort()); + Serial.print(", Length: "); + Serial.print(packet.length()); + Serial.print(", Data: "); + Serial.write(packet.data(), packet.length()); + Serial.println(); + //reply to the client + packet.printf("Got %u bytes of data", packet.length()); +} + +void setup() +{ + Serial.begin(115200); + + while (!Serial && (millis() < 5000)); + + Serial.print(F("\nStart AsyncUDPMulticastServer on ")); + Serial.print(ARDUINO_BOARD); + Serial.print(F(" with ")); + Serial.println(SHIELD_TYPE); + Serial.println(WEBSERVER_ESP32_W6100_VERSION); + Serial.println(ASYNC_UDP_ESP32_W6100_VERSION); + + Serial.setDebugOutput(true); + + UDP_LOGWARN(F("Default SPI pinout:")); + UDP_LOGWARN1(F("SPI_HOST:"), ETH_SPI_HOST); + UDP_LOGWARN1(F("MOSI:"), MOSI_GPIO); + UDP_LOGWARN1(F("MISO:"), MISO_GPIO); + UDP_LOGWARN1(F("SCK:"), SCK_GPIO); + UDP_LOGWARN1(F("CS:"), CS_GPIO); + UDP_LOGWARN1(F("INT:"), INT_GPIO); + UDP_LOGWARN1(F("SPI Clock (MHz):"), SPI_CLOCK_MHZ); + UDP_LOGWARN(F("=========================")); + + /////////////////////////////////// + + // To be called before ETH.begin() + ESP32_W6100_onEvent(); + + // start the ethernet connection and the server: + // Use DHCP dynamic IP and random mac + //bool begin(int MISO_GPIO, int MOSI_GPIO, int SCLK_GPIO, int CS_GPIO, int INT_GPIO, int SPI_CLOCK_MHZ, + // int SPI_HOST, uint8_t *W6100_Mac = W6100_Default_Mac); + ETH.begin( MISO_GPIO, MOSI_GPIO, SCK_GPIO, CS_GPIO, INT_GPIO, SPI_CLOCK_MHZ, ETH_SPI_HOST ); + //ETH.begin( MISO_GPIO, MOSI_GPIO, SCK_GPIO, CS_GPIO, INT_GPIO, SPI_CLOCK_MHZ, ETH_SPI_HOST, mac[millis() % NUMBER_OF_MAC] ); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + //ETH.config(myIP, myGW, mySN, myDNS); + + ESP32_W6100_waitForConnect(); + + /////////////////////////////////// + + // Client address + Serial.print("Async_UDPClient started @ IP address: "); + Serial.println(ETH.localIP()); + + + if (udp.listenMulticast(IPAddress(239, 1, 2, 3), 1234)) + { + Serial.print("UDP Listening on IP: "); + Serial.println(ETH.localIP()); + + udp.onPacket([](AsyncUDPPacket packet) + { + parsePacket(packet); + }); + + //Send multicast + udp.print("Hello!"); + } +} + +void loop() +{ + delay(1000); + //Send multicast + udp.print("Anyone here?"); +} diff --git a/examples/AsyncUDPServer/AsyncUDPServer.ino b/examples/AsyncUDPServer/AsyncUDPServer.ino new file mode 100644 index 0000000..e3b6999 --- /dev/null +++ b/examples/AsyncUDPServer/AsyncUDPServer.ino @@ -0,0 +1,164 @@ +/**************************************************************************************************************************** + Async_UdpServer.ino + + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license + *****************************************************************************************************************************/ + +#if !( defined(ESP32) ) + #error This code is designed for (ESP32 + W6100) to run on ESP32 platform! Please check your Tools->Board setting. +#endif + +#include + +#define ASYNC_UDP_ESP32_W6100_DEBUG_PORT Serial + +// Use from 0 to 4. Higher number, more debugging messages and memory usage. +#define _ASYNC_UDP_ESP32_W6100_LOGLEVEL_ 1 + +////////////////////////////////////////////////////////// + +// Optional values to override default settings +// Don't change unless you know what you're doing +//#define ETH_SPI_HOST SPI3_HOST +//#define SPI_CLOCK_MHZ 25 + +// Must connect INT to GPIOxx or not working +//#define INT_GPIO 4 + +//#define MISO_GPIO 19 +//#define MOSI_GPIO 23 +//#define SCK_GPIO 18 +//#define CS_GPIO 5 + +////////////////////////////////////////////////////////// + +#include + +///////////////////////////////////////////// + +// Enter a MAC address and IP address for your controller below. +#define NUMBER_OF_MAC 20 + +byte mac[][NUMBER_OF_MAC] = +{ + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x01 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x02 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x03 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x04 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x05 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x06 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x07 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x08 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x09 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0A }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0B }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0C }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0D }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0E }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0F }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x10 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x11 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x12 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x13 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x14 }, +}; + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +///////////////////////////////////////////// + +AsyncUDP udp; + +void setup() +{ + Serial.begin(115200); + + while (!Serial && (millis() < 5000)); + + Serial.print(F("\nStart Async_UdpServer on ")); + Serial.print(ARDUINO_BOARD); + Serial.print(F(" with ")); + Serial.println(SHIELD_TYPE); + Serial.println(WEBSERVER_ESP32_W6100_VERSION); + Serial.println(ASYNC_UDP_ESP32_W6100_VERSION); + + Serial.setDebugOutput(true); + + UDP_LOGWARN(F("Default SPI pinout:")); + UDP_LOGWARN1(F("SPI_HOST:"), ETH_SPI_HOST); + UDP_LOGWARN1(F("MOSI:"), MOSI_GPIO); + UDP_LOGWARN1(F("MISO:"), MISO_GPIO); + UDP_LOGWARN1(F("SCK:"), SCK_GPIO); + UDP_LOGWARN1(F("CS:"), CS_GPIO); + UDP_LOGWARN1(F("INT:"), INT_GPIO); + UDP_LOGWARN1(F("SPI Clock (MHz):"), SPI_CLOCK_MHZ); + UDP_LOGWARN(F("=========================")); + + /////////////////////////////////// + + // To be called before ETH.begin() + ESP32_W6100_onEvent(); + + // start the ethernet connection and the server: + // Use DHCP dynamic IP and random mac + //bool begin(int MISO_GPIO, int MOSI_GPIO, int SCLK_GPIO, int CS_GPIO, int INT_GPIO, int SPI_CLOCK_MHZ, + // int SPI_HOST, uint8_t *W6100_Mac = W6100_Default_Mac); + ETH.begin( MISO_GPIO, MOSI_GPIO, SCK_GPIO, CS_GPIO, INT_GPIO, SPI_CLOCK_MHZ, ETH_SPI_HOST ); + //ETH.begin( MISO_GPIO, MOSI_GPIO, SCK_GPIO, CS_GPIO, INT_GPIO, SPI_CLOCK_MHZ, ETH_SPI_HOST, mac[millis() % NUMBER_OF_MAC] ); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + //ETH.config(myIP, myGW, mySN, myDNS); + + ESP32_W6100_waitForConnect(); + + /////////////////////////////////// + + // Client address + Serial.print("AsyncUDPServer started @ IP address: "); + Serial.println(ETH.localIP()); + + if (udp.listen(1234)) + { + Serial.print("UDP Listening on IP: "); + Serial.println(ETH.localIP()); + + udp.onPacket([](AsyncUDPPacket packet) + { + Serial.print("UDP Packet Type: "); + Serial.print(packet.isBroadcast() ? "Broadcast" : packet.isMulticast() ? "Multicast" : "Unicast"); + Serial.print(", From: "); + Serial.print(packet.remoteIP()); + Serial.print(":"); + Serial.print(packet.remotePort()); + Serial.print(", To: "); + Serial.print(packet.localIP()); + Serial.print(":"); + Serial.print(packet.localPort()); + Serial.print(", Length: "); + Serial.print(packet.length()); + Serial.print(", Data: "); + Serial.write(packet.data(), packet.length()); + Serial.println(); + //reply to the client + packet.printf("Got %u bytes of data", packet.length()); + }); + } +} + +void loop() +{ + delay(1000); + //Send broadcast + udp.broadcast("Anyone here?"); +} diff --git a/examples/AsyncUdpNTPClient/AsyncUdpNTPClient.ino b/examples/AsyncUdpNTPClient/AsyncUdpNTPClient.ino new file mode 100644 index 0000000..4625eaf --- /dev/null +++ b/examples/AsyncUdpNTPClient/AsyncUdpNTPClient.ino @@ -0,0 +1,244 @@ +/**************************************************************************************************************************** + AsyncUdpNTPClient.ino + + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license + *****************************************************************************************************************************/ + +#if !( defined(ESP32) ) + #error This code is designed for (ESP32 + W6100) to run on ESP32 platform! Please check your Tools->Board setting. +#endif + +#include + +#define ASYNC_UDP_ESP32_W6100_DEBUG_PORT Serial + +// Use from 0 to 4. Higher number, more debugging messages and memory usage. +#define _ASYNC_UDP_ESP32_W6100_LOGLEVEL_ 1 + +////////////////////////////////////////////////////////// + +// Optional values to override default settings +// Don't change unless you know what you're doing +//#define ETH_SPI_HOST SPI3_HOST +//#define SPI_CLOCK_MHZ 25 + +// Must connect INT to GPIOxx or not working +//#define INT_GPIO 4 + +//#define MISO_GPIO 19 +//#define MOSI_GPIO 23 +//#define SCK_GPIO 18 +//#define CS_GPIO 5 + +////////////////////////////////////////////////////////// + +#include + +///////////////////////////////////////////// + +// Enter a MAC address and IP address for your controller below. +#define NUMBER_OF_MAC 20 + +byte mac[][NUMBER_OF_MAC] = +{ + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x01 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x02 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x03 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x04 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x05 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x06 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x07 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x08 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x09 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0A }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0B }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0C }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0D }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0E }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0F }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x10 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x11 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x12 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x13 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x14 }, +}; + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +///////////////////////////////////////////// + +#include + +// 0.ca.pool.ntp.org +IPAddress timeServerIP = IPAddress(208, 81, 1, 244); +// time.nist.gov +//IPAddress timeServerIP = IPAddress(132, 163, 96, 1); + +#define NTP_REQUEST_PORT 123 + +const int NTP_PACKET_SIZE = 48; // NTP timestamp is in the first 48 bytes of the message + +byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets + +// A UDP instance to let us send and receive packets over UDP +AsyncUDP Udp; + +// send an NTP request to the time server at the given address +void createNTPpacket(void) +{ + Serial.println("============= createNTPpacket ============="); + + // set all bytes in the buffer to 0 + memset(packetBuffer, 0, NTP_PACKET_SIZE); + // Initialize values needed to form NTP request + // (see URL above for details on the packets) + + packetBuffer[0] = 0b11100011; // LI, Version, Mode + packetBuffer[1] = 0; // Stratum, or type of clock + packetBuffer[2] = 6; // Polling Interval + packetBuffer[3] = 0xEC; // Peer Clock Precision + + // 8 bytes of zero for Root Delay & Root Dispersion + packetBuffer[12] = 49; + packetBuffer[13] = 0x4E; + packetBuffer[14] = 49; + packetBuffer[15] = 52; +} + +void parsePacket(AsyncUDPPacket packet) +{ + struct tm ts; + char buf[80]; + + memcpy(packetBuffer, packet.data(), sizeof(packetBuffer)); + + Serial.print("Received UDP Packet Type: "); + Serial.println(packet.isBroadcast() ? "Broadcast" : packet.isMulticast() ? "Multicast" : "Unicast"); + Serial.print("From: "); + Serial.print(packet.remoteIP()); + Serial.print(":"); + Serial.print(packet.remotePort()); + Serial.print(", To: "); + Serial.print(packet.localIP()); + Serial.print(":"); + Serial.print(packet.localPort()); + Serial.print(", Length: "); + Serial.print(packet.length()); + Serial.println(); + + unsigned long highWord = word(packetBuffer[40], packetBuffer[41]); + unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]); + + // combine the four bytes (two words) into a long integer + // this is NTP time (seconds since Jan 1 1900): + unsigned long secsSince1900 = highWord << 16 | lowWord; + + Serial.print(F("Seconds since Jan 1 1900 = ")); + Serial.println(secsSince1900); + + // now convert NTP time into )everyday time: + Serial.print(F("Epoch/Unix time = ")); + + // Unix time starts on Jan 1 1970. In seconds, that's 2208988800: + const unsigned long seventyYears = 2208988800UL; + + // subtract seventy years: + unsigned long epoch = secsSince1900 - seventyYears; + time_t epoch_t = epoch; //secsSince1900 - seventyYears; + + // print Unix time: + Serial.println(epoch); + + // print the hour, minute and second: + Serial.print(F("The UTC/GMT time is ")); // UTC is the time at Greenwich Meridian (GMT) + + ts = *localtime(&epoch_t); + strftime(buf, sizeof(buf), "%a %Y-%m-%d %H:%M:%S %Z", &ts); + Serial.println(buf); +} + +void sendNTPPacket(void) +{ + createNTPpacket(); + //Send unicast + Udp.write(packetBuffer, sizeof(packetBuffer)); +} + +void setup() +{ + Serial.begin(115200); + + while (!Serial && (millis() < 5000)); + + Serial.print(F("\nStart AsyncUdpNTPClient on ")); + Serial.print(ARDUINO_BOARD); + Serial.print(F(" with ")); + Serial.println(SHIELD_TYPE); + Serial.println(WEBSERVER_ESP32_W6100_VERSION); + Serial.println(ASYNC_UDP_ESP32_W6100_VERSION); + + Serial.setDebugOutput(true); + + UDP_LOGWARN(F("Default SPI pinout:")); + UDP_LOGWARN1(F("SPI_HOST:"), ETH_SPI_HOST); + UDP_LOGWARN1(F("MOSI:"), MOSI_GPIO); + UDP_LOGWARN1(F("MISO:"), MISO_GPIO); + UDP_LOGWARN1(F("SCK:"), SCK_GPIO); + UDP_LOGWARN1(F("CS:"), CS_GPIO); + UDP_LOGWARN1(F("INT:"), INT_GPIO); + UDP_LOGWARN1(F("SPI Clock (MHz):"), SPI_CLOCK_MHZ); + UDP_LOGWARN(F("=========================")); + + /////////////////////////////////// + + // To be called before ETH.begin() + ESP32_W6100_onEvent(); + + // start the ethernet connection and the server: + // Use DHCP dynamic IP and random mac + //bool begin(int MISO_GPIO, int MOSI_GPIO, int SCLK_GPIO, int CS_GPIO, int INT_GPIO, int SPI_CLOCK_MHZ, + // int SPI_HOST, uint8_t *W6100_Mac = W6100_Default_Mac); + ETH.begin( MISO_GPIO, MOSI_GPIO, SCK_GPIO, CS_GPIO, INT_GPIO, SPI_CLOCK_MHZ, ETH_SPI_HOST ); + //ETH.begin( MISO_GPIO, MOSI_GPIO, SCK_GPIO, CS_GPIO, INT_GPIO, SPI_CLOCK_MHZ, ETH_SPI_HOST, mac[millis() % NUMBER_OF_MAC] ); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + //ETH.config(myIP, myGW, mySN, myDNS); + + ESP32_W6100_waitForConnect(); + + /////////////////////////////////// + + // Client address + Serial.print("AsyncUdpNTPClient started @ IP address: "); + Serial.println(ETH.localIP()); + + //NTP requests are to port NTP_REQUEST_PORT = 123 + if (Udp.connect(timeServerIP, NTP_REQUEST_PORT)) + { + Serial.println("UDP connected"); + + Udp.onPacket([](AsyncUDPPacket packet) + { + parsePacket(packet); + }); + } +} + +void loop() +{ + sendNTPPacket(); + + // wait 60 seconds before asking for the time again + delay(60000); +} diff --git a/examples/AsyncUdpSendReceive/AsyncUdpSendReceive.ino b/examples/AsyncUdpSendReceive/AsyncUdpSendReceive.ino new file mode 100644 index 0000000..ed42cab --- /dev/null +++ b/examples/AsyncUdpSendReceive/AsyncUdpSendReceive.ino @@ -0,0 +1,262 @@ +/**************************************************************************************************************************** + AsyncUDPSendReceive.ino + + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license + *****************************************************************************************************************************/ + +#if !( defined(ESP32) ) + #error This code is designed for (ESP32 + W6100) to run on ESP32 platform! Please check your Tools->Board setting. +#endif + +#include + +#define ASYNC_UDP_ESP32_W6100_DEBUG_PORT Serial + +// Use from 0 to 4. Higher number, more debugging messages and memory usage. +#define _ASYNC_UDP_ESP32_W6100_LOGLEVEL_ 1 + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Optional values to override default settings +// Don't change unless you know what you're doing +//#define ETH_SPI_HOST SPI3_HOST +//#define SPI_CLOCK_MHZ 25 + +// Must connect INT to GPIOxx or not working +//#define INT_GPIO 4 + +//#define MISO_GPIO 19 +//#define MOSI_GPIO 23 +//#define SCK_GPIO 18 +//#define CS_GPIO 5 + +////////////////////////////////////////////////////////// + +#include + +///////////////////////////////////////////// + +// Enter a MAC address and IP address for your controller below. +#define NUMBER_OF_MAC 20 + +byte mac[][NUMBER_OF_MAC] = +{ + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x01 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x02 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x03 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x04 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x05 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x06 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x07 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x08 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x09 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0A }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0B }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0C }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0D }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x0E }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x0F }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x10 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x11 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x12 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x13 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0x14 }, +}; + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +///////////////////////////////////////////// + +#include + +// 0.ca.pool.ntp.org +IPAddress timeServerIP = IPAddress(208, 81, 1, 244); +// time.nist.gov +//IPAddress timeServerIP = IPAddress(132, 163, 96, 1); + +#define NTP_REQUEST_PORT 123 + +char ReplyBuffer[] = "ACK"; // a string to send back + +char timeServer[] = "time.nist.gov"; // NTP server + +const int NTP_PACKET_SIZE = 48; // NTP timestamp is in the first 48 bytes of the message + +byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets + +// A UDP instance to let us send and receive packets over UDP +AsyncUDP Udp; + +void sendACKPacket(void) +{ + Serial.println("============= sendACKPacket ============="); + + // Send unicast ACK to the same remoteIP and remotePort we received the packet + // The AsyncUDP_STM32 library will take care of the correct IP and port based on pcb + Udp.write((uint8_t *) ReplyBuffer, sizeof(ReplyBuffer)); +} + +// send an NTP request to the time server at the given address +void createNTPpacket(void) +{ + Serial.println("============= createNTPpacket ============="); + + // set all bytes in the buffer to 0 + memset(packetBuffer, 0, NTP_PACKET_SIZE); + // Initialize values needed to form NTP request + // (see URL above for details on the packets) + + packetBuffer[0] = 0b11100011; // LI, Version, Mode + packetBuffer[1] = 0; // Stratum, or type of clock + packetBuffer[2] = 6; // Polling Interval + packetBuffer[3] = 0xEC; // Peer Clock Precision + + // 8 bytes of zero for Root Delay & Root Dispersion + packetBuffer[12] = 49; + packetBuffer[13] = 0x4E; + packetBuffer[14] = 49; + packetBuffer[15] = 52; +} + +void sendNTPPacket(void) +{ + createNTPpacket(); + //Send unicast + Udp.write(packetBuffer, sizeof(packetBuffer)); +} + +void parsePacket(AsyncUDPPacket packet) +{ + struct tm ts; + char buf[80]; + + memcpy(packetBuffer, packet.data(), sizeof(packetBuffer)); + + Serial.print("Received UDP Packet Type: "); + Serial.println(packet.isBroadcast() ? "Broadcast" : packet.isMulticast() ? "Multicast" : "Unicast"); + Serial.print("From: "); + Serial.print(packet.remoteIP()); + Serial.print(":"); + Serial.print(packet.remotePort()); + Serial.print(", To: "); + Serial.print(packet.localIP()); + Serial.print(":"); + Serial.print(packet.localPort()); + Serial.print(", Length: "); + Serial.print(packet.length()); + Serial.println(); + + unsigned long highWord = word(packetBuffer[40], packetBuffer[41]); + unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]); + + // combine the four bytes (two words) into a long integer + // this is NTP time (seconds since Jan 1 1900): + unsigned long secsSince1900 = highWord << 16 | lowWord; + + Serial.print(F("Seconds since Jan 1 1900 = ")); + Serial.println(secsSince1900); + + // now convert NTP time into )everyday time: + Serial.print(F("Epoch/Unix time = ")); + + // Unix time starts on Jan 1 1970. In seconds, that's 2208988800: + const unsigned long seventyYears = 2208988800UL; + + // subtract seventy years: + unsigned long epoch = secsSince1900 - seventyYears; + time_t epoch_t = epoch; //secsSince1900 - seventyYears; + + // print Unix time: + Serial.println(epoch); + + // print the hour, minute and second: + Serial.print(F("The UTC/GMT time is ")); // UTC is the time at Greenwich Meridian (GMT) + + ts = *localtime(&epoch_t); + strftime(buf, sizeof(buf), "%a %Y-%m-%d %H:%M:%S %Z", &ts); + Serial.println(buf); + + // send a reply, to the IP address and port that sent us the packet we received + sendACKPacket(); +} + +void setup() +{ + Serial.begin(115200); + + while (!Serial && (millis() < 5000)); + + Serial.print(F("\nStart AsyncUDPSendReceive on ")); + Serial.print(ARDUINO_BOARD); + Serial.print(F(" with ")); + Serial.println(SHIELD_TYPE); + Serial.println(WEBSERVER_ESP32_W6100_VERSION); + Serial.println(ASYNC_UDP_ESP32_W6100_VERSION); + + Serial.setDebugOutput(true); + + UDP_LOGWARN(F("Default SPI pinout:")); + UDP_LOGWARN1(F("SPI_HOST:"), ETH_SPI_HOST); + UDP_LOGWARN1(F("MOSI:"), MOSI_GPIO); + UDP_LOGWARN1(F("MISO:"), MISO_GPIO); + UDP_LOGWARN1(F("SCK:"), SCK_GPIO); + UDP_LOGWARN1(F("CS:"), CS_GPIO); + UDP_LOGWARN1(F("INT:"), INT_GPIO); + UDP_LOGWARN1(F("SPI Clock (MHz):"), SPI_CLOCK_MHZ); + UDP_LOGWARN(F("=========================")); + + /////////////////////////////////// + + // To be called before ETH.begin() + ESP32_W6100_onEvent(); + + // start the ethernet connection and the server: + // Use DHCP dynamic IP and random mac + //bool begin(int MISO_GPIO, int MOSI_GPIO, int SCLK_GPIO, int CS_GPIO, int INT_GPIO, int SPI_CLOCK_MHZ, + // int SPI_HOST, uint8_t *W6100_Mac = W6100_Default_Mac); + ETH.begin( MISO_GPIO, MOSI_GPIO, SCK_GPIO, CS_GPIO, INT_GPIO, SPI_CLOCK_MHZ, ETH_SPI_HOST ); + //ETH.begin( MISO_GPIO, MOSI_GPIO, SCK_GPIO, CS_GPIO, INT_GPIO, SPI_CLOCK_MHZ, ETH_SPI_HOST, mac[millis() % NUMBER_OF_MAC] ); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + //ETH.config(myIP, myGW, mySN, myDNS); + + ESP32_W6100_waitForConnect(); + + /////////////////////////////////// + + // Client address + Serial.print("AsyncUDPSendReceive started @ IP address: "); + Serial.println(ETH.localIP()); + + Serial.println(F("\nStarting connection to server...")); + + //NTP requests are to port NTP_REQUEST_PORT = 123 + if (Udp.connect(timeServerIP, NTP_REQUEST_PORT)) + { + Serial.println("UDP connected"); + + Udp.onPacket([](AsyncUDPPacket packet) + { + parsePacket(packet); + }); + } +} + +void loop() +{ + sendNTPPacket(); + + // wait 60 seconds before asking for the time again + delay(60000); +} diff --git a/examples/multiFileProject/multiFileProject.cpp b/examples/multiFileProject/multiFileProject.cpp new file mode 100644 index 0000000..6c32bce --- /dev/null +++ b/examples/multiFileProject/multiFileProject.cpp @@ -0,0 +1,12 @@ +/**************************************************************************************************************************** + multiFileProject.cpp + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license +*****************************************************************************************************************************/ + +// To demo how to include files in multi-file Projects + +#include "multiFileProject.h" diff --git a/examples/multiFileProject/multiFileProject.h b/examples/multiFileProject/multiFileProject.h new file mode 100644 index 0000000..2cf2adc --- /dev/null +++ b/examples/multiFileProject/multiFileProject.h @@ -0,0 +1,17 @@ +/**************************************************************************************************************************** + multiFileProject.h + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license +*****************************************************************************************************************************/ + +// To demo how to include files in multi-file Projects + +#pragma once + +#define _ASYNC_UDP_ESP32_ENC_LOGLEVEL_ 1 + +// Can be included as many times as necessary, without `Multiple Definitions` Linker Error +#include "AsyncUDP_ESP32_W6100.hpp" diff --git a/examples/multiFileProject/multiFileProject.ino b/examples/multiFileProject/multiFileProject.ino new file mode 100644 index 0000000..1eca008 --- /dev/null +++ b/examples/multiFileProject/multiFileProject.ino @@ -0,0 +1,73 @@ +/**************************************************************************************************************************** + multiFileProject.ino + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license +*****************************************************************************************************************************/ + +// To demo how to include files in multi-file Projects + +#include "multiFileProject.h" + +// Can be included as many times as necessary, without `Multiple Definitions` Linker Error +#include "AsyncUDP_ESP32_W6100.h" + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +void setup() +{ + Serial.begin(115200); + + while (!Serial && (millis() < 5000)); + + Serial.print(F("\nStart multiFileProject on ")); + Serial.print(ARDUINO_BOARD); + Serial.print(F(" with ")); + Serial.println(SHIELD_TYPE); + Serial.println(WEBSERVER_ESP32_W6100_VERSION); + Serial.println(ASYNC_UDP_ESP32_W6100_VERSION); + + Serial.setDebugOutput(true); + + UDP_LOGWARN(F("Default SPI pinout:")); + UDP_LOGWARN1(F("SPI_HOST:"), ETH_SPI_HOST); + UDP_LOGWARN1(F("MOSI:"), MOSI_GPIO); + UDP_LOGWARN1(F("MISO:"), MISO_GPIO); + UDP_LOGWARN1(F("SCK:"), SCK_GPIO); + UDP_LOGWARN1(F("CS:"), CS_GPIO); + UDP_LOGWARN1(F("INT:"), INT_GPIO); + UDP_LOGWARN1(F("SPI Clock (MHz):"), SPI_CLOCK_MHZ); + UDP_LOGWARN(F("=========================")); + + /////////////////////////////////// + + // To be called before ETH.begin() + ESP32_W6100_onEvent(); + + //bool begin(int MISO_GPIO, int MOSI_GPIO, int SCLK_GPIO, int CS_GPIO, int INT_GPIO, int SPI_CLOCK_MHZ, + // int SPI_HOST, uint8_t *W6100_Mac = W6100_Default_Mac); + ETH.begin( MISO_GPIO, MOSI_GPIO, SCK_GPIO, CS_GPIO, INT_GPIO, SPI_CLOCK_MHZ, ETH_SPI_HOST ); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + //ETH.config(myIP, myGW, mySN, myDNS); + + ESP32_W6100_waitForConnect(); + + /////////////////////////////////// + + Serial.print("You're OK now"); +} + +void loop() +{ + // put your main code here, to run repeatedly: +} diff --git a/keywords.txt b/keywords.txt new file mode 100644 index 0000000..fcc6ab0 --- /dev/null +++ b/keywords.txt @@ -0,0 +1,70 @@ +####################################### +# Syntax Coloring Map For Ultrasound +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +AsyncUDP KEYWORD1 +AsyncUDPPacket KEYWORD1 +AsyncUDPMessage KEYWORD1 +ip_addr_t KEYWORD1 + +AuPacketHandlerFunction KEYWORD1 +AuPacketHandlerFunctionWithArg KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +################### +# AsyncUDPMessage +################### + +write KEYWORD2 +space KEYWORD2 +data KEYWORD2 +length KEYWORD2 +flush KEYWORD2 + +################### +# AsyncUDPPacket +################### + +data KEYWORD2 +length KEYWORD2 +isBroadcast KEYWORD2 +isMulticast KEYWORD2 +localIP KEYWORD2 +localPort KEYWORD2 +remoteIP KEYWORD2 +remotePort KEYWORD2 +send KEYWORD2 +write KEYWORD2 + +################### +# AsyncUDPPacket +################### + +onPacket KEYWORD2 +listen KEYWORD2 +listenMulticast KEYWORD2 +connect KEYWORD2 +close KEYWORD2 +write KEYWORD2 +broadcastTo KEYWORD2 +broadcast KEYWORD2 +sendTo KEYWORD2 +send KEYWORD2 +connected KEYWORD2 + +################### +# Functions +################### + +ESP32_ENC_Event KEYWORD2 + +####################################### +# Constants (LITERAL1) +####################################### diff --git a/library.json b/library.json new file mode 100644 index 0000000..e9823ab --- /dev/null +++ b/library.json @@ -0,0 +1,46 @@ +{ + "name":"AsyncUDP_ESP32_W6100", + "version": "2.0.0", + "description":"Fully Asynchronous UDP Library for ESP32 boards using LwIP W6100 Ethernet. The library is easy to use and includes support for Unicast, Broadcast and Multicast environments", + "keywords":"communication, data, async, udp, ntp, time, time-server, server, client, multicast, broadcast, webserver, esp32, esp32-s2, esp32-c3, w6100, lwip, lwip-w6100, udp-server, udp-multicast-server", + "authors": + [ + { + "name": "Hristo Gochkov", + "url": "https://github.com/me-no-dev" + }, + { + "name": "Khoi Hoang", + "url": "https://github.com/khoih-prog", + "email": "khoih.prog@gmail.com", + "maintainer": true + } + ], + "repository": + { + "type": "git", + "url": "https://github.com/khoih-prog/AsyncUDP_ESP32_W6100" + }, + "homepage": "https://github.com/khoih-prog/AsyncUDP_ESP32_W6100", + "export": { + "exclude": [ + "linux", + "extras", + "tests" + ] + }, + "dependencies": + [ + { + "owner": "khoih-prog", + "name": "WebServer_ESP32_W6100", + "version": "^1.5.2", + "platforms": "espressif32" + } + ], + "license": "LGPL-3.0", + "frameworks": "*", + "platforms": ["espressif32"], + "examples": "examples/*/*/*.ino", + "headers": [ "AsyncUDP_ESP32_W6100.h", "AsyncUDP_ESP32_W6100.hpp" ] +} diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..704b15d --- /dev/null +++ b/library.properties @@ -0,0 +1,13 @@ +name=AsyncUDP_ESP32_W6100 +version=2.0.0 +author=Hristo Gochkov,Khoi Hoang +maintainer=Khoi Hoang +sentence=Fully Asynchronous UDP Library for ESP32 boards using LwIP W6100 Ethernet +paragraph=The library is easy to use and includes support for Unicast, Broadcast and Multicast environments. +category=Communication +url=https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 +repository=https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 +architectures=esp32 +depends=WebServer_ESP32_W6100 +license=GPLv3 +includes=AsyncUDP_ESP32_W6100.h, AsyncUDP_ESP32_W6100.hpp diff --git a/pics/W6100.png b/pics/W6100.png new file mode 100644 index 0000000000000000000000000000000000000000..867119addd8cc0cce0efa1346a65fd6b977f1c22 GIT binary patch literal 118672 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\>cQ_|&5X~0gOmfu)F{5y2FdkK)Hb(| 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%0AH@AP`nGZcX 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

-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 literal 0 HcmV?d00001 diff --git a/platformio/platformio.ini b/platformio/platformio.ini new file mode 100644 index 0000000..7dad816 --- /dev/null +++ b/platformio/platformio.ini @@ -0,0 +1,155 @@ +;PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +; ============================================================ +; chose environment: +; ESP32 +; esp32s2 +; esp32s3 +; esp32c3 + +; ============================================================ +default_envs = ESP32 + +[env] +; ============================================================ +; Serial configuration +; choose upload speed, serial-monitor speed +; ============================================================ +upload_speed = 921600 +;upload_port = COM11 +;monitor_speed = 9600 +;monitor_port = COM11 + +; Checks for the compatibility with frameworks and dev/platforms +lib_compat_mode = strict +lib_ldf_mode = chain+ +;lib_ldf_mode = deep+ + +lib_deps = +; PlatformIO 4.x +; WebServer_ESP32_W6100@~1.5.2 +; PlatformIO 5.x + khoih-prog/WebServer_ESP32_W6100@~1.5.2 + +build_flags = +; set your debug output (default=Serial) + -D DEBUG_ESP_PORT=Serial +; comment the following line to enable WiFi debugging + -D NDEBUG + +[env:ESP32] +platform = espressif32 +framework = arduino + +; ============================================================ +; Board configuration +; choose your board by uncommenting one of the following lines +; ============================================================ +;board = esp32cam +;board = alksesp32 +;board = featheresp32 +;board = espea32 +;board = bpi-bit +;board = d-duino-32 +board = esp32doit-devkit-v1 +;board = pocket_32 +;board = fm-devkit +;board = pico32 +;board = esp32-evb +;board = esp32-gateway +;board = esp32-pro +;board = esp32-poe +;board = oroca_edubot +;board = onehorse32dev +;board = lopy +;board = lopy4 +;board = wesp32 +;board = esp32thing +;board = sparkfun_lora_gateway_1-channel +;board = ttgo-lora32-v1 +;board = ttgo-t-beam +;board = turta_iot_node +;board = lolin_d32 +;board = lolin_d32_pro +;board = lolin32 +;board = wemosbat +;board = widora-air +;board = xinabox_cw02 +;board = iotbusio +;board = iotbusproteus +;board = nina_w10 + +[env:esp32s2] +platform = espressif32 +framework = arduino + +; toolchain download links see +; refer "name": "xtensa-esp32s2-elf-gcc","version": "gcc8_4_0-esp-2021r1" section of +; https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json +; e.g. Windows: https://github.com/espressif/crosstool-NG/releases/download/esp-2021r1/xtensa-esp32s2-elf-gcc8_4_0-esp-2021r1-win32.zip +platform_packages = + toolchain-xtensa32s2@file://C:\Users\Max\Downloads\xtensa-esp32s2-elf + framework-arduinoespressif32@https://github.com/espressif/arduino-esp32.git#a4118ea88987c28aac3a49bcb9cc5d6c0acc6f3f + platformio/tool-esptoolpy @ ~1.30100 +framework = arduino +board = esp32dev +board_build.mcu = esp32s2 +board_build.partitions = huge_app.csv +board_build.variant = esp32s2 +board_build.f_cpu = 240000000L +board_build.f_flash = 80000000L +board_build.flash_mode = qio +board_build.arduino.ldscript = esp32s2_out.ld +build_unflags = + -DARDUINO_ESP32_DEV + -DARDUINO_VARIANT="esp32" +build_flags = + -DARDUINO_ESP32S2_DEV + -DARDUINO_VARIANT="esp32s2" + + +[env:esp32s3] +platform = espressif32 +framework = arduino + +board_build.mcu = esp32s3 +board_build.partitions = huge_app.csv +board_build.variant = esp32s3 +board_build.f_cpu = 240000000L +board_build.f_flash = 80000000L +board_build.flash_mode = qio +board_build.arduino.ldscript = esp32s3_out.ld +build_unflags = + -DARDUINO_ESP32_DEV + -DARDUINO_VARIANT="esp32" +build_flags = + -DARDUINO_ESP32S3_DEV + -DARDUINO_VARIANT="esp32s3" + + +[env:esp32sc3] +platform = espressif32 +framework = arduino + +board_build.mcu = esp32c3 +board_build.partitions = huge_app.csv +board_build.variant = esp32c3 +board_build.f_cpu = 160000000L +board_build.f_flash = 80000000L +board_build.flash_mode = qio +board_build.arduino.ldscript = esp32c3_out.ld +build_unflags = + -DARDUINO_ESP32_DEV + -DARDUINO_VARIANT="esp32" +build_flags = + -DARDUINO_ESP32S3_DEV + -DARDUINO_VARIANT="esp32c3" diff --git a/src/AsyncUDP_ESP32_W6100.h b/src/AsyncUDP_ESP32_W6100.h new file mode 100644 index 0000000..181cf05 --- /dev/null +++ b/src/AsyncUDP_ESP32_W6100.h @@ -0,0 +1,72 @@ +/**************************************************************************************************************************** + AsyncUDP_ESP32_W6100.h + + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license + + Version: 2.0.0 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 2.0.0 K Hoang 10/01/2023 Initial coding for ESP32_W6100. Bump up version to v2.0.0 to sync with AsyncUDP v2.0.0 + *****************************************************************************************************************************/ + +#pragma once + +#ifndef ASYNC_UDP_ESP32_W6100_H +#define ASYNC_UDP_ESP32_W6100_H + +//////////////////////////////////////////////// + +#if ( ( defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2) ) && ( ARDUINO_ESP32_GIT_VER != 0x46d5afb1 ) ) + +#if (_ASYNC_UDP_ESP32_W6100_LOGLEVEL_ > 2) + #warning Using code for ESP32 core v2.0.0+ in AsyncUDP_ESP32_W6100.h +#endif + +#define ASYNC_UDP_ESP32_W6100_VERSION "AsyncUDP_ESP32_W6100 v2.0.0 for core v2.0.0+" + +extern "C" +{ +#include "lwip/ip_addr.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +} + +#else + +#if (_ASYNC_UDP_ESP32_W6100_LOGLEVEL_ > 2) + #warning Using code for ESP32 core v1.0.6- in AsyncUDP_ESP32_W6100.h +#endif + +#define ASYNC_UDP_ESP32_W6100_VERSION "AsyncUDP_ESP32_W6100 v2.0.0 for core v1.0.6-" + +extern "C" +{ +#include "lwip/ip_addr.h" +#include +#include "freertos/queue.h" +#include "freertos/semphr.h" +} +#endif + +#include "IPAddress.h" +#include "IPv6Address.h" +#include "Print.h" +#include + +//////////////////////////////////////////////// + +#include // https://github.com/khoih-prog/WebServer_ESP32_W6100 + +#include "AsyncUDP_ESP32_W6100_Debug.h" + +#include "AsyncUDP_ESP32_W6100.hpp" +#include "AsyncUDP_ESP32_W6100_Impl.h" + +//////////////////////////////////////////////// + +#endif //ASYNC_UDP_ESP32_W6100_H diff --git a/src/AsyncUDP_ESP32_W6100.hpp b/src/AsyncUDP_ESP32_W6100.hpp new file mode 100644 index 0000000..a7abb45 --- /dev/null +++ b/src/AsyncUDP_ESP32_W6100.hpp @@ -0,0 +1,183 @@ +/**************************************************************************************************************************** + AsyncUDP_ESP32_W6100.hpp + + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license + + Version: 2.0.0 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 2.0.0 K Hoang 10/01/2023 Initial coding for ESP32_W6100. Bump up version to v2.0.0 to sync with AsyncUDP v2.0.0 + *****************************************************************************************************************************/ + +#pragma once + +#ifndef ASYNC_UDP_ESP32_W6100_HPP +#define ASYNC_UDP_ESP32_W6100_HPP + +//////////////////////////////////////////////// + +#include // https://github.com/khoih-prog/WebServer_ESP32_W6100 + +class AsyncUDP; +class AsyncUDPPacket; +class AsyncUDPMessage; +struct udp_pcb; +struct pbuf; +struct netif; + +typedef std::function AuPacketHandlerFunction; +typedef std::function AuPacketHandlerFunctionWithArg; + +//////////////////////////////////////////////// + +class AsyncUDPMessage : public Print +{ + protected: + uint8_t * _buffer; + size_t _index; + size_t _size; + + public: + AsyncUDPMessage(size_t size = CONFIG_TCP_MSS); + virtual ~AsyncUDPMessage(); + + size_t write(const uint8_t *data, size_t len); + size_t write(uint8_t data); + size_t space(); + uint8_t * data(); + size_t length(); + void flush(); + + operator bool() + { + return _buffer != NULL; + } +}; + +//////////////////////////////////////////////// + +class AsyncUDPPacket : public Stream +{ + protected: + + AsyncUDP * _udp; + pbuf * _pb; + tcpip_adapter_if_t _if; + ip_addr_t _localIp; + uint16_t _localPort; + ip_addr_t _remoteIp; + uint16_t _remotePort; + uint8_t _remoteMac[6]; + uint8_t * _data; + size_t _len; + size_t _index; + + public: + AsyncUDPPacket(AsyncUDPPacket &packet); + AsyncUDPPacket(AsyncUDP *udp, pbuf *pb, const ip_addr_t *addr, uint16_t port, struct netif * netif); + virtual ~AsyncUDPPacket(); + + uint8_t * data(); + size_t length(); + bool isBroadcast(); + bool isMulticast(); + bool isIPv6(); + + tcpip_adapter_if_t interface(); + + IPAddress localIP(); + IPv6Address localIPv6(); + uint16_t localPort(); + IPAddress remoteIP(); + IPv6Address remoteIPv6(); + uint16_t remotePort(); + void remoteMac(uint8_t * mac); + + size_t send(AsyncUDPMessage &message); + + int available(); + size_t read(uint8_t *data, size_t len); + int read(); + int peek(); + void flush(); + + size_t write(const uint8_t *data, size_t len); + size_t write(uint8_t data); +}; + +//////////////////////////////////////////////// + +class AsyncUDP : public Print +{ + protected: + + udp_pcb *_pcb; + //xSemaphoreHandle _lock; + bool _connected; + esp_err_t _lastErr; + AuPacketHandlerFunction _handler; + + bool _init(); + void _recv(udp_pcb *upcb, pbuf *pb, const ip_addr_t *addr, uint16_t port, struct netif * netif); + + public: + + AsyncUDP(); + virtual ~AsyncUDP(); + + void onPacket(AuPacketHandlerFunctionWithArg cb, void * arg = NULL); + void onPacket(AuPacketHandlerFunction cb); + + bool listen(const ip_addr_t *addr, uint16_t port); + bool listen(const IPAddress addr, uint16_t port); + bool listen(const IPv6Address addr, uint16_t port); + bool listen(uint16_t port); + + bool listenMulticast(const ip_addr_t *addr, uint16_t port, uint8_t ttl = 1, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + bool listenMulticast(const IPAddress addr, uint16_t port, uint8_t ttl = 1, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + bool listenMulticast(const IPv6Address addr, uint16_t port, uint8_t ttl = 1, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + + bool connect(const ip_addr_t *addr, uint16_t port); + bool connect(const IPAddress addr, uint16_t port); + bool connect(const IPv6Address addr, uint16_t port); + + void close(); + + size_t writeTo(const uint8_t *data, size_t len, const ip_addr_t *addr, uint16_t port, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + size_t writeTo(const uint8_t *data, size_t len, const IPAddress addr, uint16_t port, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + size_t writeTo(const uint8_t *data, size_t len, const IPv6Address addr, uint16_t port, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + size_t write(const uint8_t *data, size_t len); + size_t write(uint8_t data); + + size_t broadcastTo(uint8_t *data, size_t len, uint16_t port, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + size_t broadcastTo(const char * data, uint16_t port, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + size_t broadcast(uint8_t *data, size_t len); + size_t broadcast(const char * data); + + size_t sendTo(AsyncUDPMessage &message, const ip_addr_t *addr, uint16_t port, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + size_t sendTo(AsyncUDPMessage &message, const IPAddress addr, uint16_t port, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + size_t sendTo(AsyncUDPMessage &message, const IPv6Address addr, uint16_t port, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + size_t send(AsyncUDPMessage &message); + + size_t broadcastTo(AsyncUDPMessage &message, uint16_t port, tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX); + size_t broadcast(AsyncUDPMessage &message); + + IPAddress listenIP(); + IPv6Address listenIPv6(); + + bool connected(); + esp_err_t lastErr(); + operator bool(); + + static void _s_recv(void *arg, udp_pcb *upcb, pbuf *p, const ip_addr_t *addr, uint16_t port, struct netif * netif); +}; + +//////////////////////////////////////////////// + + +#endif //ASYNC_UDP_ESP32_W6100_HPP diff --git a/src/AsyncUDP_ESP32_W6100_Debug.h b/src/AsyncUDP_ESP32_W6100_Debug.h new file mode 100644 index 0000000..3c8c667 --- /dev/null +++ b/src/AsyncUDP_ESP32_W6100_Debug.h @@ -0,0 +1,93 @@ +/**************************************************************************************************************************** + AsyncUDP_Debug_ESP32_W6100.h + + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license + + Version: 2.0.0 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 2.0.0 K Hoang 10/01/2023 Initial coding for ESP32_W6100. Bump up version to v2.0.0 to sync with AsyncUDP v2.0.0 + *****************************************************************************************************************************/ + +#pragma once + +#ifndef Async_UDP_ESP32_W6100_Debug_H +#define Async_UDP_ESP32_W6100_Debug_H + +#ifdef ASYNC_UDP_ESP32_W6100_DEBUG_PORT + #define ASYNC_UDP_DBG_PORT ASYNC_UDP_ESP32_W6100_DEBUG_PORT +#else + #define ASYNC_UDP_DBG_PORT Serial +#endif + +// Change _ASYNC_UDP_ESP32_W6100_LOGLEVEL_ to set tracing and logging verbosity +// 0: DISABLED: no logging +// 1: ERROR: errors +// 2: WARN: errors and warnings +// 3: INFO: errors, warnings and informational (default) +// 4: DEBUG: errors, warnings, informational and debug + +#ifndef _ASYNC_UDP_ESP32_W6100_LOGLEVEL_ + #define _ASYNC_UDP_ESP32_W6100_LOGLEVEL_ 0 +#endif + +/////////////////////////////////////// + +const char ASYNC_UDP_MARK[] = "[UDP] "; +const char ASYNC_UDP_SP[] = " "; + +#define ASYNC_UDP_PRINT ASYNC_UDP_DBG_PORT.print +#define ASYNC_UDP_PRINTLN ASYNC_UDP_DBG_PORT.println +#define ASYNC_UDP_FLUSH ASYNC_UDP_DBG_PORT.flush + +#define ASYNC_UDP_PRINT_MARK ASYNC_UDP_PRINT(ASYNC_UDP_MARK) +#define ASYNC_UDP_PRINT_SP ASYNC_UDP_PRINT(ASYNC_UDP_SP) + +/////////////////////////////////////// + +#define ASYNC_UDP_LOG(x) { ASYNC_UDP_PRINTLN(x); } +#define ASYNC_UDP_LOG0(x) { ASYNC_UDP_PRINT(x); } +#define ASYNC_UDP_LOG1(x,y) { ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINTLN(y); } +#define ASYNC_UDP_LOG2(x,y,z) { ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT(y); ASYNC_UDP_PRINTLN(z); } +#define ASYNC_UDP_LOG3(x,y,z,w) { ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT(y); ASYNC_UDP_PRINT(z); ASYNC_UDP_PRINTLN(w); } + +/////////////////////////////////////// + +#define UDP_LOGERROR(x) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>0) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINTLN(x); } +#define UDP_LOGERROR0(x) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>0) { ASYNC_UDP_PRINT(x); } +#define UDP_LOGERROR1(x,y) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>0) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(y); } +#define UDP_LOGERROR2(x,y,z) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>0) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(y); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(z); } +#define UDP_LOGERROR3(x,y,z,w) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>0) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(y); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(z); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(w); } + +/////////////////////////////////////// + +#define UDP_LOGWARN(x) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>1) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINTLN(x); } +#define UDP_LOGWARN0(x) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>1) { ASYNC_UDP_PRINT(x); } +#define UDP_LOGWARN1(x,y) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>1) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(y); } +#define UDP_LOGWARN2(x,y,z) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>1) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(y); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(z); } +#define UDP_LOGWARN3(x,y,z,w) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>1) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(y); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(z); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(w); } + +/////////////////////////////////////// + +#define UDP_LOGINFO(x) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>2) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINTLN(x); } +#define UDP_LOGINFO0(x) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>2) { ASYNC_UDP_PRINT(x); } +#define UDP_LOGINFO1(x,y) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>2) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(y); } +#define UDP_LOGINFO2(x,y,z) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>2) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(y); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(z); } +#define UDP_LOGINFO3(x,y,z,w) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>2) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(y); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(z); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(w); } + +/////////////////////////////////////// + +#define UDP_LOGDEBUG(x) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>3) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINTLN(x); } +#define UDP_LOGDEBUG0(x) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>3) { ASYNC_UDP_PRINT(x); } +#define UDP_LOGDEBUG1(x,y) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>3) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(y); } +#define UDP_LOGDEBUG2(x,y,z) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>3) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(y); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(z); } +#define UDP_LOGDEBUG3(x,y,z,w) if(_ASYNC_UDP_ESP32_W6100_LOGLEVEL_>3) { ASYNC_UDP_PRINT_MARK; ASYNC_UDP_PRINT(x); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(y); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINT(z); ASYNC_UDP_PRINT_SP; ASYNC_UDP_PRINTLN(w); } + +/////////////////////////////////////// + +#endif //Async_UDP_ESP32_W6100_Debug_H diff --git a/src/AsyncUDP_ESP32_W6100_Impl.h b/src/AsyncUDP_ESP32_W6100_Impl.h new file mode 100644 index 0000000..fbef367 --- /dev/null +++ b/src/AsyncUDP_ESP32_W6100_Impl.h @@ -0,0 +1,1443 @@ +/**************************************************************************************************************************** + AsyncUDP_ESP32_W6100_Impl.h + + AsyncUDP_ESP32_W6100 is a Async UDP library for the ESP32_W6100 (ESP32 + LwIP W6100) + + Based on and modified from ESPAsyncUDP Library (https://github.com/me-no-dev/ESPAsyncUDP) + Built by Khoi Hoang https://github.com/khoih-prog/AsyncUDP_ESP32_W6100 + Licensed under GPLv3 license + + Version: 2.0.0 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 2.0.0 K Hoang 10/01/2023 Initial coding for ESP32_W6100. Bump up version to v2.0.0 to sync with AsyncUDP v2.0.0 + *****************************************************************************************************************************/ + +#pragma once + +#ifndef ASYNC_UDP_ESP32_W6100_IMPL_H +#define ASYNC_UDP_ESP32_W6100_IMPL_H + +//////////////////////////////////////////////// + +extern "C" +{ +#include "lwip/opt.h" +#include "lwip/inet.h" +#include "lwip/udp.h" +#include "lwip/igmp.h" +#include "lwip/ip_addr.h" +#include "lwip/mld6.h" +#include "lwip/prot/ethernet.h" +#include +#include +} + +//////////////////////////////////////////////// + +#include "lwip/priv/tcpip_priv.h" + +/* + typedef int32_t esp_err_t; + + // + #define ESP_OK 0 + #define ESP_FAIL -1 + + #define ESP_ERR_NO_MEM 0x101 // Out of memory + #define ESP_ERR_INVALID_ARG 0x102 // Invalid argument + #define ESP_ERR_INVALID_STATE 0x103 // Invalid state + #define ESP_ERR_INVALID_SIZE 0x104 // Invalid size + #define ESP_ERR_NOT_FOUND 0x105 // Requested resource not found + #define ESP_ERR_NOT_SUPPORTED 0x106 // Operation or feature not supported + #define ESP_ERR_TIMEOUT 0x107 // Operation timed out + #define ESP_ERR_INVALID_RESPONSE 0x108 // Received response was invalid + #define ESP_ERR_INVALID_CRC 0x109 // CRC or checksum was invalid + #define ESP_ERR_INVALID_VERSION 0x10A // Version was invalid + #define ESP_ERR_INVALID_MAC 0x10B // MAC address was invalid + + #define ESP_ERR_WIFI_BASE 0x3000 // Starting number of WiFi error codes + #define ESP_ERR_MESH_BASE 0x4000 // Starting number of MESH error codes +*/ + +//////////////////////////////////////////////// + +typedef struct +{ + struct tcpip_api_call_data call; + udp_pcb * pcb; + const ip_addr_t *addr; + uint16_t port; + struct pbuf *pb; + struct netif *netif; + err_t err; +} udp_api_call_t; + +//////////////////////////////////////////////// + +static err_t _udp_connect_api(struct tcpip_api_call_data *api_call_msg) +{ + udp_api_call_t * msg = (udp_api_call_t *)api_call_msg; + msg->err = udp_connect(msg->pcb, msg->addr, msg->port); + + UDP_LOGDEBUG1(F("_udp_connect_api: Error ="), msg->err); + + return msg->err; +} + +//////////////////////////////////////////////// + +static err_t _udp_connect(struct udp_pcb *pcb, const ip_addr_t *addr, u16_t port) +{ + udp_api_call_t msg; + msg.pcb = pcb; + msg.addr = addr; + msg.port = port; + tcpip_api_call(_udp_connect_api, (struct tcpip_api_call_data*)&msg); + + UDP_LOGDEBUG1(F("_udp_connect: Error ="), msg.err); + + return msg.err; +} + +//////////////////////////////////////////////// + +static err_t _udp_disconnect_api(struct tcpip_api_call_data *api_call_msg) +{ + udp_api_call_t * msg = (udp_api_call_t *)api_call_msg; + msg->err = 0; + udp_disconnect(msg->pcb); + + UDP_LOGDEBUG1(F("_udp_disconnect_api: Error ="), msg->err); + + return msg->err; +} + +//////////////////////////////////////////////// + +static void _udp_disconnect(struct udp_pcb *pcb) +{ + udp_api_call_t msg; + msg.pcb = pcb; + tcpip_api_call(_udp_disconnect_api, (struct tcpip_api_call_data*)&msg); +} + +//////////////////////////////////////////////// + +static err_t _udp_remove_api(struct tcpip_api_call_data *api_call_msg) +{ + udp_api_call_t * msg = (udp_api_call_t *)api_call_msg; + msg->err = 0; + udp_remove(msg->pcb); + + UDP_LOGDEBUG1(F("_udp_remove_api: Error ="), msg->err); + + return msg->err; +} + +//////////////////////////////////////////////// + +static void _udp_remove(struct udp_pcb *pcb) +{ + udp_api_call_t msg; + msg.pcb = pcb; + tcpip_api_call(_udp_remove_api, (struct tcpip_api_call_data*)&msg); +} + +//////////////////////////////////////////////// + +static err_t _udp_bind_api(struct tcpip_api_call_data *api_call_msg) +{ + udp_api_call_t * msg = (udp_api_call_t *)api_call_msg; + msg->err = udp_bind(msg->pcb, msg->addr, msg->port); + + UDP_LOGDEBUG1(F("_udp_bind_api: Error ="), msg->err); + + return msg->err; +} + +//////////////////////////////////////////////// + +static err_t _udp_bind(struct udp_pcb *pcb, const ip_addr_t *addr, u16_t port) +{ + udp_api_call_t msg; + msg.pcb = pcb; + msg.addr = addr; + msg.port = port; + tcpip_api_call(_udp_bind_api, (struct tcpip_api_call_data*)&msg); + + UDP_LOGDEBUG1(F("_udp_bind: Error ="), msg.err); + + return msg.err; +} + +//////////////////////////////////////////////// + +static err_t _udp_sendto_api(struct tcpip_api_call_data *api_call_msg) +{ + udp_api_call_t * msg = (udp_api_call_t *)api_call_msg; + msg->err = udp_sendto(msg->pcb, msg->pb, msg->addr, msg->port); + + UDP_LOGDEBUG1(F("_udp_sendto_api: Error ="), msg->err); + + return msg->err; +} + +//////////////////////////////////////////////// + +static err_t _udp_sendto(struct udp_pcb *pcb, struct pbuf *pb, const ip_addr_t *addr, u16_t port) +{ + udp_api_call_t msg; + msg.pcb = pcb; + msg.addr = addr; + msg.port = port; + msg.pb = pb; + tcpip_api_call(_udp_sendto_api, (struct tcpip_api_call_data*)&msg); + + UDP_LOGDEBUG1(F("_udp_sendto: Error ="), msg.err); + + return msg.err; +} + +//////////////////////////////////////////////// + +static err_t _udp_sendto_if_api(struct tcpip_api_call_data *api_call_msg) +{ + udp_api_call_t * msg = (udp_api_call_t *)api_call_msg; + msg->err = udp_sendto_if(msg->pcb, msg->pb, msg->addr, msg->port, msg->netif); + + UDP_LOGDEBUG1(F("_udp_sendto_if_api: Error ="), msg->err); + + return msg->err; +} + +//////////////////////////////////////////////// + +static err_t _udp_sendto_if(struct udp_pcb *pcb, struct pbuf *pb, const ip_addr_t *addr, u16_t port, + struct netif *netif) +{ + udp_api_call_t msg; + msg.pcb = pcb; + msg.addr = addr; + msg.port = port; + msg.pb = pb; + msg.netif = netif; + tcpip_api_call(_udp_sendto_if_api, (struct tcpip_api_call_data*)&msg); + + UDP_LOGDEBUG1(F("_udp_sendto_if: Error ="), msg.err); + + return msg.err; +} + +//////////////////////////////////////////////// + +typedef struct +{ + void *arg; + udp_pcb *pcb; + pbuf *pb; + const ip_addr_t *addr; + uint16_t port; + struct netif * netif; +} lwip_event_packet_t; + +//////////////////////////////////////////////// + +static xQueueHandle _udp_queue; +static volatile TaskHandle_t _udp_task_handle = NULL; + +//////////////////////////////////////////////// + +static void _udp_task(void *pvParameters) +{ + lwip_event_packet_t * e = NULL; + + for (;;) + { + if (xQueueReceive(_udp_queue, &e, portMAX_DELAY) == pdTRUE) + { + if (!e->pb) + { + free((void*)(e)); + continue; + } + + AsyncUDP::_s_recv(e->arg, e->pcb, e->pb, e->addr, e->port, e->netif); + free((void*)(e)); + } + } + + _udp_task_handle = NULL; + vTaskDelete(NULL); +} + +//////////////////////////////////////////////// + +static bool _udp_task_start() +{ + if (!_udp_queue) + { + _udp_queue = xQueueCreate(32, sizeof(lwip_event_packet_t *)); + + if (!_udp_queue) + { + return false; + } + } + + if (!_udp_task_handle) + { + xTaskCreateUniversal(_udp_task, "async_udp", 4096, NULL, CONFIG_ARDUINO_UDP_TASK_PRIORITY, + (TaskHandle_t*)&_udp_task_handle, CONFIG_ARDUINO_UDP_RUNNING_CORE); + + if (!_udp_task_handle) + { + return false; + } + } + + return true; +} + +//////////////////////////////////////////////// + +static bool _udp_task_post(void *arg, udp_pcb *pcb, pbuf *pb, const ip_addr_t *addr, uint16_t port, struct netif *netif) +{ + if (!_udp_task_handle || !_udp_queue) + { + return false; + } + + lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); + + if (!e) + { + return false; + } + + e->arg = arg; + e->pcb = pcb; + e->pb = pb; + e->addr = addr; + e->port = port; + e->netif = netif; + + if (xQueueSend(_udp_queue, &e, portMAX_DELAY) != pdPASS) + { + free((void*)(e)); + + return false; + } + + return true; +} + +//////////////////////////////////////////////// + +static void _udp_recv(void *arg, udp_pcb *pcb, pbuf *pb, const ip_addr_t *addr, uint16_t port) +{ + while (pb != NULL) + { + pbuf * this_pb = pb; + pb = pb->next; + this_pb->next = NULL; + + if (!_udp_task_post(arg, pcb, this_pb, addr, port, ip_current_input_netif())) + { + pbuf_free(this_pb); + } + } +} + +//////////////////////////////////////////////// + +/* + static bool _udp_task_stop() + { + if(!_udp_task_post(NULL, NULL, NULL, NULL, 0, NULL)) + { + return false; + } + + while(_udp_task_handle) + { + vTaskDelay(10); + } + + lwip_event_packet_t * e; + + while (xQueueReceive(_udp_queue, &e, 0) == pdTRUE) + { + if(e->pb) + { + pbuf_free(e->pb); + } + + free((void*)(e)); + } + + vQueueDelete(_udp_queue); + _udp_queue = NULL; + } +*/ + +//////////////////////////////////////////////// + +#define UDP_MUTEX_LOCK() //xSemaphoreTake(_lock, portMAX_DELAY) +#define UDP_MUTEX_UNLOCK() //xSemaphoreGive(_lock) + +//////////////////////////////////////////////// +//////////////////////////////////////////////// + +AsyncUDPMessage::AsyncUDPMessage(size_t size) +{ + _index = 0; + + if (size > CONFIG_TCP_MSS) + { + size = CONFIG_TCP_MSS; + } + + _size = size; + _buffer = (uint8_t *)malloc(size); +} + +//////////////////////////////////////////////// + +AsyncUDPMessage::~AsyncUDPMessage() +{ + if (_buffer) + { + free(_buffer); + } +} + +//////////////////////////////////////////////// + +size_t AsyncUDPMessage::write(const uint8_t *data, size_t len) +{ + if (_buffer == NULL) + { + UDP_LOGDEBUG(F("write: Error NULL _buffer")); + + return 0; + } + + size_t s = space(); + + if (len > s) + { + len = s; + } + + memcpy(_buffer + _index, data, len); + _index += len; + + return len; +} + +//////////////////////////////////////////////// + +size_t AsyncUDPMessage::write(uint8_t data) +{ + return write(&data, 1); +} + +//////////////////////////////////////////////// + +size_t AsyncUDPMessage::space() +{ + if (_buffer == NULL) + { + UDP_LOGDEBUG(F("space: Error NULL _buffer")); + + return 0; + } + + return _size - _index; +} + +//////////////////////////////////////////////// + +uint8_t * AsyncUDPMessage::data() +{ + return _buffer; +} + +//////////////////////////////////////////////// + +size_t AsyncUDPMessage::length() +{ + return _index; +} + +//////////////////////////////////////////////// + +void AsyncUDPMessage::flush() +{ + _index = 0; +} + +//////////////////////////////////////////////// +//////////////////////////////////////////////// + +AsyncUDPPacket::AsyncUDPPacket(AsyncUDPPacket &packet) +{ + _udp = packet._udp; + _pb = packet._pb; + _if = packet._if; + _data = packet._data; + _len = packet._len; + _index = 0; + + memcpy(&_remoteIp, &packet._remoteIp, sizeof(ip_addr_t)); + memcpy(&_localIp, &packet._localIp, sizeof(ip_addr_t)); + _localPort = packet._localPort; + _remotePort = packet._remotePort; + memcpy(_remoteMac, packet._remoteMac, 6); + + pbuf_ref(_pb); +} + +//////////////////////////////////////////////// + +AsyncUDPPacket::AsyncUDPPacket(AsyncUDP *udp, pbuf *pb, const ip_addr_t *raddr, uint16_t rport, struct netif * ntif) +{ + _udp = udp; + _pb = pb; + _if = TCPIP_ADAPTER_IF_MAX; + _data = (uint8_t*)(pb->payload); + _len = pb->len; + _index = 0; + + pbuf_ref(_pb); + + //memcpy(&_remoteIp, raddr, sizeof(ip_addr_t)); + _remoteIp.type = raddr->type; + _localIp.type = _remoteIp.type; + + eth_hdr* eth = NULL; + udp_hdr* udphdr = (udp_hdr *)(_data - UDP_HLEN); + _localPort = ntohs(udphdr->dest); + _remotePort = ntohs(udphdr->src); + + if (_remoteIp.type == IPADDR_TYPE_V4) + { + eth = (eth_hdr *)(_data - UDP_HLEN - IP_HLEN - SIZEOF_ETH_HDR); + struct ip_hdr * iphdr = (struct ip_hdr *)(_data - UDP_HLEN - IP_HLEN); + _localIp.u_addr.ip4.addr = iphdr->dest.addr; + _remoteIp.u_addr.ip4.addr = iphdr->src.addr; + } + else + { + eth = (eth_hdr *)(_data - UDP_HLEN - IP6_HLEN - SIZEOF_ETH_HDR); + struct ip6_hdr * ip6hdr = (struct ip6_hdr *)(_data - UDP_HLEN - IP6_HLEN); + memcpy(&_localIp.u_addr.ip6.addr, (uint8_t *)ip6hdr->dest.addr, 16); + memcpy(&_remoteIp.u_addr.ip6.addr, (uint8_t *)ip6hdr->src.addr, 16); + } + + memcpy(_remoteMac, eth->src.addr, 6); + + struct netif * netif = NULL; + void * nif = NULL; + int i; + + for (i = 0; i < TCPIP_ADAPTER_IF_MAX; i++) + { + tcpip_adapter_get_netif ((tcpip_adapter_if_t)i, &nif); + netif = (struct netif *)nif; + + if (netif && netif == ntif) + { + _if = (tcpip_adapter_if_t)i; + break; + } + } +} + +//////////////////////////////////////////////// + +AsyncUDPPacket::~AsyncUDPPacket() +{ + pbuf_free(_pb); +} + +//////////////////////////////////////////////// + +uint8_t * AsyncUDPPacket::data() +{ + return _data; +} + +//////////////////////////////////////////////// + +size_t AsyncUDPPacket::length() +{ + return _len; +} + +//////////////////////////////////////////////// + +int AsyncUDPPacket::available() +{ + return _len - _index; +} + +//////////////////////////////////////////////// + +size_t AsyncUDPPacket::read(uint8_t *data, size_t len) +{ + size_t i; + size_t a = _len - _index; + + if (len > a) + { + len = a; + } + + for (i = 0; i < len; i++) + { + data[i] = read(); + } + + return len; +} + +//////////////////////////////////////////////// + +int AsyncUDPPacket::read() +{ + if (_index < _len) + { + return _data[_index++]; + } + + return -1; +} + +//////////////////////////////////////////////// + +int AsyncUDPPacket::peek() +{ + if (_index < _len) + { + return _data[_index]; + } + + return -1; +} + +//////////////////////////////////////////////// + +void AsyncUDPPacket::flush() +{ + _index = _len; +} + +//////////////////////////////////////////////// + +tcpip_adapter_if_t AsyncUDPPacket::interface() +{ + return _if; +} + +//////////////////////////////////////////////// + +IPAddress AsyncUDPPacket::localIP() +{ + if (_localIp.type != IPADDR_TYPE_V4) + { + return IPAddress(); + } + + return IPAddress(_localIp.u_addr.ip4.addr); +} + +//////////////////////////////////////////////// + +IPv6Address AsyncUDPPacket::localIPv6() +{ + if (_localIp.type != IPADDR_TYPE_V6) + { + return IPv6Address(); + } + + return IPv6Address(_localIp.u_addr.ip6.addr); +} + +//////////////////////////////////////////////// + +uint16_t AsyncUDPPacket::localPort() +{ + return _localPort; +} + +//////////////////////////////////////////////// + +IPAddress AsyncUDPPacket::remoteIP() +{ + if (_remoteIp.type != IPADDR_TYPE_V4) + { + return IPAddress(); + } + + return IPAddress(_remoteIp.u_addr.ip4.addr); +} + +//////////////////////////////////////////////// + +IPv6Address AsyncUDPPacket::remoteIPv6() +{ + if (_remoteIp.type != IPADDR_TYPE_V6) + { + return IPv6Address(); + } + + return IPv6Address(_remoteIp.u_addr.ip6.addr); +} + +//////////////////////////////////////////////// + +uint16_t AsyncUDPPacket::remotePort() +{ + return _remotePort; +} + +//////////////////////////////////////////////// + +void AsyncUDPPacket::remoteMac(uint8_t * mac) +{ + memcpy(mac, _remoteMac, 6); +} + +//////////////////////////////////////////////// + +bool AsyncUDPPacket::isIPv6() +{ + return _localIp.type == IPADDR_TYPE_V6; +} + +//////////////////////////////////////////////// + +bool AsyncUDPPacket::isBroadcast() +{ + if (_localIp.type == IPADDR_TYPE_V6) + { + return false; + } + + uint32_t ip = _localIp.u_addr.ip4.addr; + + return ip == 0xFFFFFFFF || ip == 0 || (ip & 0xFF000000) == 0xFF000000; +} + +//////////////////////////////////////////////// + +bool AsyncUDPPacket::isMulticast() +{ + return ip_addr_ismulticast(&(_localIp)); +} + +//////////////////////////////////////////////// + +size_t AsyncUDPPacket::write(const uint8_t *data, size_t len) +{ + if (!data) + { + UDP_LOGDEBUG(F("AsyncUDPPacket::write: Error NULL data")); + + return 0; + } + + return _udp->writeTo(data, len, &_remoteIp, _remotePort, _if); +} + +//////////////////////////////////////////////// + +size_t AsyncUDPPacket::write(uint8_t data) +{ + return write(&data, 1); +} + +//////////////////////////////////////////////// + +size_t AsyncUDPPacket::send(AsyncUDPMessage &message) +{ + return write(message.data(), message.length()); +} + +//////////////////////////////////////////////// +//////////////////////////////////////////////// + +bool AsyncUDP::_init() +{ + if (_pcb) + { + return true; + } + + _pcb = udp_new(); + + if (!_pcb) + { + return false; + } + + //_lock = xSemaphoreCreateMutex(); + udp_recv(_pcb, &_udp_recv, (void *) this); + + return true; +} + +//////////////////////////////////////////////// + +AsyncUDP::AsyncUDP() +{ + _pcb = NULL; + _connected = false; + _lastErr = ERR_OK; + _handler = NULL; +} + +//////////////////////////////////////////////// + +AsyncUDP::~AsyncUDP() +{ + close(); + UDP_MUTEX_LOCK(); + + udp_recv(_pcb, NULL, NULL); + _udp_remove(_pcb); + _pcb = NULL; + + UDP_MUTEX_UNLOCK(); + //vSemaphoreDelete(_lock); +} + +//////////////////////////////////////////////// + +void AsyncUDP::close() +{ + UDP_MUTEX_LOCK(); + + if (_pcb != NULL) + { + if (_connected) + { + _udp_disconnect(_pcb); + } + + _connected = false; + //todo: unjoin multicast group + } + + UDP_MUTEX_UNLOCK(); +} + +//////////////////////////////////////////////// + +bool AsyncUDP::connect(const ip_addr_t *addr, uint16_t port) +{ + if (!_udp_task_start()) + { + log_e("failed to start task"); + UDP_LOGERROR(F("AsyncUDP::connect: failed to start task")); + + return false; + } + + if (!_init()) + { + UDP_LOGERROR(F("AsyncUDP::connect: failed to init")); + + return false; + } + + close(); + UDP_MUTEX_LOCK(); + + _lastErr = _udp_connect(_pcb, addr, port); + + if (_lastErr != ERR_OK) + { + UDP_MUTEX_UNLOCK(); + + UDP_LOGERROR(F("AsyncUDP::connect: _udp_connect failed")); + + return false; + } + + _connected = true; + + UDP_MUTEX_UNLOCK(); + + return true; +} + +//////////////////////////////////////////////// + +bool AsyncUDP::listen(const ip_addr_t *addr, uint16_t port) +{ + if (!_udp_task_start()) + { + log_e("failed to start task"); + UDP_LOGERROR(F("AsyncUDP::listen: failed to start task")); + + return false; + } + + if (!_init()) + { + UDP_LOGERROR(F("AsyncUDP::listen: failed to init")); + + return false; + } + + close(); + + if (addr) + { + IP_SET_TYPE_VAL(_pcb->local_ip, addr->type); + IP_SET_TYPE_VAL(_pcb->remote_ip, addr->type); + } + + UDP_MUTEX_LOCK(); + + if (_udp_bind(_pcb, addr, port) != ERR_OK) + { + UDP_MUTEX_UNLOCK(); + + UDP_LOGERROR(F("AsyncUDP::listen: failed to _udp_bind")); + + return false; + } + + _connected = true; + + UDP_MUTEX_UNLOCK(); + + return true; +} + +//////////////////////////////////////////////// + +static esp_err_t joinMulticastGroup(const ip_addr_t *addr, bool join, + tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_MAX) +{ + struct netif * netif = NULL; + + if (tcpip_if < TCPIP_ADAPTER_IF_MAX) + { + void * nif = NULL; + esp_err_t err = tcpip_adapter_get_netif(tcpip_if, &nif); + + if (err) + { + UDP_LOGERROR(F("joinMulticastGroup: failed to get_netif")); + + return ESP_ERR_INVALID_ARG; + } + + netif = (struct netif *)nif; + + if (addr->type == IPADDR_TYPE_V4) + { + if (join) + { + if (igmp_joingroup_netif(netif, (const ip4_addr *) & (addr->u_addr.ip4))) + { + UDP_LOGERROR(F("joinMulticastGroup: IPv4 failed to joingroup")); + + return ESP_ERR_INVALID_STATE; + } + } + else + { + if (igmp_leavegroup_netif(netif, (const ip4_addr *) & (addr->u_addr.ip4))) + { + UDP_LOGERROR(F("joinMulticastGroup: IPv4 failed to leavegroup")); + + return ESP_ERR_INVALID_STATE; + } + } + } + else + { + if (join) + { + if (mld6_joingroup_netif(netif, &(addr->u_addr.ip6))) + { + UDP_LOGERROR(F("joinMulticastGroup: IPv6 failed to joingroup")); + + return ESP_ERR_INVALID_STATE; + } + } + else + { + if (mld6_leavegroup_netif(netif, &(addr->u_addr.ip6))) + { + UDP_LOGERROR(F("joinMulticastGroup: IPv6 failed to leavegroup")); + + return ESP_ERR_INVALID_STATE; + } + } + } + } + else + { + if (addr->type == IPADDR_TYPE_V4) + { + if (join) + { + if (igmp_joingroup((const ip4_addr *)IP4_ADDR_ANY, (const ip4_addr *) & (addr->u_addr.ip4))) + { + return ESP_ERR_INVALID_STATE; + } + } + else + { + if (igmp_leavegroup((const ip4_addr *)IP4_ADDR_ANY, (const ip4_addr *) & (addr->u_addr.ip4))) + { + return ESP_ERR_INVALID_STATE; + } + } + } + else + { + if (join) + { + if (mld6_joingroup((const ip6_addr *)IP6_ADDR_ANY, &(addr->u_addr.ip6))) + { + return ESP_ERR_INVALID_STATE; + } + } + else + { + if (mld6_leavegroup((const ip6_addr *)IP6_ADDR_ANY, &(addr->u_addr.ip6))) + { + return ESP_ERR_INVALID_STATE; + } + } + } + } + + return ESP_OK; +} + +//////////////////////////////////////////////// + +bool AsyncUDP::listenMulticast(const ip_addr_t *addr, uint16_t port, uint8_t ttl, tcpip_adapter_if_t tcpip_if) +{ + if (!ip_addr_ismulticast(addr)) + { + UDP_LOGERROR(F("listenMulticast: not addr_ismulticast")); + + return false; + } + + if (joinMulticastGroup(addr, true, tcpip_if) != ERR_OK) + { + UDP_LOGERROR(F("listenMulticast: error joinMulticast")); + + return false; + } + + if (!listen(NULL, port)) + { + UDP_LOGERROR1(F("listenMulticast: error listen to port ="), port); + + return false; + } + + UDP_MUTEX_LOCK(); + + _pcb->mcast_ttl = ttl; + _pcb->remote_port = port; + ip_addr_copy(_pcb->remote_ip, *addr); + //ip_addr_copy(_pcb->remote_ip, ip_addr_any_type); + + UDP_MUTEX_UNLOCK(); + + return true; +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::writeTo(const uint8_t * data, size_t len, const ip_addr_t * addr, uint16_t port, + tcpip_adapter_if_t tcpip_if) +{ + if (!_pcb) + { + UDP_MUTEX_LOCK(); + + _pcb = udp_new(); + + UDP_MUTEX_UNLOCK(); + + if (_pcb == NULL) + { + return 0; + } + } + + if (len > CONFIG_TCP_MSS) + { + len = CONFIG_TCP_MSS; + } + + _lastErr = ERR_OK; + pbuf* pbt = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM); + + if (pbt != NULL) + { + uint8_t* dst = reinterpret_cast(pbt->payload); + memcpy(dst, data, len); + + UDP_MUTEX_LOCK(); + + if (tcpip_if < TCPIP_ADAPTER_IF_MAX) + { + void * nif = NULL; + tcpip_adapter_get_netif((tcpip_adapter_if_t)tcpip_if, &nif); + + if (!nif) + { + _lastErr = _udp_sendto(_pcb, pbt, addr, port); + } + else + { + _lastErr = _udp_sendto_if(_pcb, pbt, addr, port, (struct netif *)nif); + } + } + else + { + _lastErr = _udp_sendto(_pcb, pbt, addr, port); + } + + UDP_MUTEX_UNLOCK(); + + pbuf_free(pbt); + + if (_lastErr < ERR_OK) + { + UDP_LOGERROR1(F("AsyncUDP::writeTo: _lastErr ="), _lastErr); + + return 0; + } + + UDP_LOGDEBUG1(F("AsyncUDP::writeTo: len ="), len); + + return len; + } + + UDP_LOGERROR(F("AsyncUDP::writeTo: Error NULL pbt")); + + return 0; +} + +//////////////////////////////////////////////// + +void AsyncUDP::_recv(udp_pcb *upcb, pbuf *pb, const ip_addr_t *addr, uint16_t port, struct netif * netif) +{ + while (pb != NULL) + { + pbuf * this_pb = pb; + pb = pb->next; + this_pb->next = NULL; + + if (_handler) + { + AsyncUDPPacket packet(this, this_pb, addr, port, netif); + _handler(packet); + } + + pbuf_free(this_pb); + } +} + +//////////////////////////////////////////////// + +void AsyncUDP::_s_recv(void *arg, udp_pcb *upcb, pbuf *p, const ip_addr_t *addr, uint16_t port, struct netif * netif) +{ + reinterpret_cast(arg)->_recv(upcb, p, addr, port, netif); +} + +//////////////////////////////////////////////// + +bool AsyncUDP::listen(uint16_t port) +{ + return listen(IP_ANY_TYPE, port); +} + +//////////////////////////////////////////////// + +bool AsyncUDP::listen(const IPAddress addr, uint16_t port) +{ + ip_addr_t laddr; + laddr.type = IPADDR_TYPE_V4; + laddr.u_addr.ip4.addr = addr; + + return listen(&laddr, port); +} + +//////////////////////////////////////////////// + +bool AsyncUDP::listenMulticast(const IPAddress addr, uint16_t port, uint8_t ttl, tcpip_adapter_if_t tcpip_if) +{ + ip_addr_t laddr; + laddr.type = IPADDR_TYPE_V4; + laddr.u_addr.ip4.addr = addr; + + return listenMulticast(&laddr, port, ttl, tcpip_if); +} + +//////////////////////////////////////////////// + +bool AsyncUDP::connect(const IPAddress addr, uint16_t port) +{ + ip_addr_t daddr; + daddr.type = IPADDR_TYPE_V4; + daddr.u_addr.ip4.addr = addr; + + return connect(&daddr, port); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::writeTo(const uint8_t *data, size_t len, const IPAddress addr, uint16_t port, + tcpip_adapter_if_t tcpip_if) +{ + ip_addr_t daddr; + daddr.type = IPADDR_TYPE_V4; + daddr.u_addr.ip4.addr = addr; + + return writeTo(data, len, &daddr, port, tcpip_if); +} + +//////////////////////////////////////////////// + +IPAddress AsyncUDP::listenIP() +{ + if (!_pcb || _pcb->remote_ip.type != IPADDR_TYPE_V4) + { + return IPAddress(); + } + + return IPAddress(_pcb->remote_ip.u_addr.ip4.addr); +} + +//////////////////////////////////////////////// + +bool AsyncUDP::listen(const IPv6Address addr, uint16_t port) +{ + ip_addr_t laddr; + laddr.type = IPADDR_TYPE_V6; + memcpy((uint8_t*)(laddr.u_addr.ip6.addr), (const uint8_t*)addr, 16); + + return listen(&laddr, port); +} + +//////////////////////////////////////////////// + +bool AsyncUDP::listenMulticast(const IPv6Address addr, uint16_t port, uint8_t ttl, tcpip_adapter_if_t tcpip_if) +{ + ip_addr_t laddr; + laddr.type = IPADDR_TYPE_V6; + memcpy((uint8_t*)(laddr.u_addr.ip6.addr), (const uint8_t*)addr, 16); + + return listenMulticast(&laddr, port, ttl, tcpip_if); +} + +//////////////////////////////////////////////// + +bool AsyncUDP::connect(const IPv6Address addr, uint16_t port) +{ + ip_addr_t daddr; + daddr.type = IPADDR_TYPE_V6; + memcpy((uint8_t*)(daddr.u_addr.ip6.addr), (const uint8_t*)addr, 16); + + return connect(&daddr, port); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::writeTo(const uint8_t *data, size_t len, const IPv6Address addr, uint16_t port, + tcpip_adapter_if_t tcpip_if) +{ + ip_addr_t daddr; + daddr.type = IPADDR_TYPE_V6; + memcpy((uint8_t*)(daddr.u_addr.ip6.addr), (const uint8_t*)addr, 16); + + return writeTo(data, len, &daddr, port, tcpip_if); +} + +//////////////////////////////////////////////// + +IPv6Address AsyncUDP::listenIPv6() +{ + if (!_pcb || _pcb->remote_ip.type != IPADDR_TYPE_V6) + { + return IPv6Address(); + } + + return IPv6Address(_pcb->remote_ip.u_addr.ip6.addr); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::write(const uint8_t *data, size_t len) +{ + return writeTo(data, len, &(_pcb->remote_ip), _pcb->remote_port); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::write(uint8_t data) +{ + return write(&data, 1); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::broadcastTo(uint8_t *data, size_t len, uint16_t port, tcpip_adapter_if_t tcpip_if) +{ + return writeTo(data, len, IP_ADDR_BROADCAST, port, tcpip_if); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::broadcastTo(const char * data, uint16_t port, tcpip_adapter_if_t tcpip_if) +{ + return broadcastTo((uint8_t *)data, strlen(data), port, tcpip_if); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::broadcast(uint8_t *data, size_t len) +{ + if (_pcb->local_port != 0) + { + return broadcastTo(data, len, _pcb->local_port); + } + + return 0; +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::broadcast(const char * data) +{ + return broadcast((uint8_t *)data, strlen(data)); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::sendTo(AsyncUDPMessage &message, const ip_addr_t *addr, uint16_t port, tcpip_adapter_if_t tcpip_if) +{ + if (!message) + { + return 0; + } + + return writeTo(message.data(), message.length(), addr, port, tcpip_if); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::sendTo(AsyncUDPMessage &message, const IPAddress addr, uint16_t port, tcpip_adapter_if_t tcpip_if) +{ + if (!message) + { + return 0; + } + + return writeTo(message.data(), message.length(), addr, port, tcpip_if); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::sendTo(AsyncUDPMessage &message, const IPv6Address addr, uint16_t port, tcpip_adapter_if_t tcpip_if) +{ + if (!message) + { + return 0; + } + + return writeTo(message.data(), message.length(), addr, port, tcpip_if); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::send(AsyncUDPMessage &message) +{ + if (!message) + { + return 0; + } + + return writeTo(message.data(), message.length(), &(_pcb->remote_ip), _pcb->remote_port); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::broadcastTo(AsyncUDPMessage &message, uint16_t port, tcpip_adapter_if_t tcpip_if) +{ + if (!message) + { + return 0; + } + + return broadcastTo(message.data(), message.length(), port, tcpip_if); +} + +//////////////////////////////////////////////// + +size_t AsyncUDP::broadcast(AsyncUDPMessage &message) +{ + if (!message) + { + return 0; + } + + return broadcast(message.data(), message.length()); +} + +//////////////////////////////////////////////// + +AsyncUDP::operator bool() +{ + return _connected; +} + +//////////////////////////////////////////////// + +bool AsyncUDP::connected() +{ + return _connected; +} + +//////////////////////////////////////////////// + +esp_err_t AsyncUDP::lastErr() +{ + return _lastErr; +} + +//////////////////////////////////////////////// + +void AsyncUDP::onPacket(AuPacketHandlerFunctionWithArg cb, void * arg) +{ + onPacket(std::bind(cb, arg, std::placeholders::_1)); +} + +//////////////////////////////////////////////// + +void AsyncUDP::onPacket(AuPacketHandlerFunction cb) +{ + _handler = cb; +} + +//////////////////////////////////////////////// + +#endif // ASYNC_UDP_ESP32_W6100_IMPL_H diff --git a/utils/astyle_library.conf b/utils/astyle_library.conf new file mode 100644 index 0000000..8a73bc2 --- /dev/null +++ b/utils/astyle_library.conf @@ -0,0 +1,70 @@ +# Code formatting rules for Arduino libraries, modified from for KH libraries: +# +# https://github.com/arduino/Arduino/blob/master/build/shared/examples_formatter.conf +# + +# astyle --style=allman -s2 -t2 -C -S -xW -Y -M120 -f -p -xg -H -xb -c --xC120 -xL *.h *.cpp *.ino + +--mode=c +--lineend=linux +--style=allman + +# -r or -R +#--recursive + +# -c => Converts tabs into spaces +convert-tabs + +# -s2 => 2 spaces indentation +--indent=spaces=2 + +# -t2 => tab =2 spaces +#--indent=tab=2 + +# -C +--indent-classes + +# -S +--indent-switches + +# -xW +--indent-preproc-block + +# -Y => indent classes, switches (and cases), comments starting at column 1 +--indent-col1-comments + +# -M120 => maximum of 120 spaces to indent a continuation line +--max-continuation-indent=120 + +# -xC120 => max‑code‑length will break a line if the code exceeds # characters +--max-code-length=120 + +# -f => +--break-blocks + +# -p => put a space around operators +--pad-oper + +# -xg => Insert space padding after commas +--pad-comma + +# -H => put a space after if/for/while +pad-header + +# -xb => Break one line headers (e.g. if/for/while) +--break-one-line-headers + +# -c => Converts tabs into spaces +#--convert-tabs + +# if you like one-liners, keep them +#keep-one-line-statements + +# -xV +--attach-closing-while + +#unpad-paren + +# -xp +remove-comment-prefix + diff --git a/utils/restyle.sh b/utils/restyle.sh new file mode 100644 index 0000000..bcd846f --- /dev/null +++ b/utils/restyle.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +for dir in . ; do + find $dir -type f \( -name "*.c" -o -name "*.h" -o -name "*.cpp" -o -name "*.ino" \) -exec astyle --suffix=none --options=./utils/astyle_library.conf \{\} \; +done +