diff --git a/lib/UdpListener/library.properties b/lib/UdpListener/library.properties new file mode 100644 index 000000000..1d453bc6c --- /dev/null +++ b/lib/UdpListener/library.properties @@ -0,0 +1,7 @@ +name=UdpListener +version=1.0 +author=Ivan Grokhotkov, Stephan Hadinger +maintainer=Stephan +sentence=UdpListener optimized for static and limite memory allocation, to reduce memory footprint of receiving SSDP request, as a replacement for WifiUdp. +paragraph=This class only handles receiving UDP Multicast packets. For sending packets, use WifiUdp. +architectures=esp8266 diff --git a/lib/UdpListener/src/UdpListener.h b/lib/UdpListener/src/UdpListener.h new file mode 100644 index 000000000..a369a5d81 --- /dev/null +++ b/lib/UdpListener/src/UdpListener.h @@ -0,0 +1,209 @@ +/* + UdpListener.h - webserver for Tasmota + + Copyright (C) 2020 Theo Arends & Stephan Hadinger + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see .@ +*/ + +// adapted from: +/* + UdpContext.h - UDP connection handling on top of lwIP + + Copyright (c) 2014 Ivan Grokhotkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + 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 +*/ + +/* + * This is a stripped down version of Udp handler to avoid overflowing + * memory when lots of multicast SSDP packets arrive. + * The pbuf is freed immediately upon arrival of the packet. + * + * Packet data are kept in a statically area in RAM and keeps + * only the first bytes (200 by default) of each packet. + * The number of packets treated is limited (3 by default), any + * new packet arriving is dropped. + * + * This class does only receiving multicast packets for LWIP2 +*/ + +#ifndef UDPMULTICASTLISTENER_H +#define UDPMULTICASTLISTENER_H + +#ifdef ESP8266 +// #include + +extern "C" { +#include +#include +} + +template +struct UdpPacket { + IPAddress srcaddr; + IPAddress dstaddr; + int16_t srcport; + netif* input_netif; + size_t len; + uint8_t buf[PACKET_SIZE]; +}; + +template +class UdpListener +{ +public: + + typedef std::function rxhandler_t; + + UdpListener(size_t packet_number) + : _pcb(0) + , _packet_number(packet_number) + , _buffers(nullptr) + , _udp_packets(0) + , _udp_ready(false) + , _udp_index(0) + { + _packet_number = packet_number; + _buffers = new UdpPacket[_packet_number]; + _pcb = udp_new(); + } + + ~UdpListener() + { + udp_remove(_pcb); + _pcb = 0; + delete[] _buffers; + _buffers = nullptr; + } + + void reset(void) + { + _udp_packets = 0; + _udp_index = 0; + } + + bool listen(const IPAddress& addr, uint16_t port) + { + if (!_buffers) { return false; } + udp_recv(_pcb, &_s_recv, (void *) this); + err_t err = udp_bind(_pcb, addr, port); + return err == ERR_OK; + } + + void disconnect() + { + udp_disconnect(_pcb); + } + + bool next() + { + if (!_buffers) { return false; } + if (_udp_packets > 0) { + if (!_udp_ready) { + // we just consume the first packet + _udp_ready = true; + } else { + _udp_packets--; + _udp_index = (_udp_index + 1) % _packet_number; // advance to next buffer index in ring + if (_udp_packets == 0) { + _udp_ready = false; + } + } + } else { + _udp_ready = false; + } + return _udp_ready; + } + + UdpPacket * read(void) + { + if (!_buffers) { return nullptr; } + if (_udp_ready) { // we have a packet ready to consume + return &_buffers[_udp_index]; + } else { + return nullptr; + } + } + +private: + + void _recv(udp_pcb *upcb, pbuf *pb, + const ip_addr_t *srcaddr, u16_t srcport) + { + if (!_buffers) { pbuf_free(pb); return; } + // Serial.printf(">>> _recv: _udp_packets = %d, _udp_index = %d, tot_len = %d\n", _udp_packets, _udp_index, pb->tot_len); + if (_udp_packets >= _packet_number) { + // we don't have slots anymore, drop packet + pbuf_free(pb); + return; + } + + uint8_t next_slot = (_udp_index + _udp_packets) % _packet_number; + + size_t packet_len = pb->tot_len; + if (packet_len > PACKET_SIZE) { packet_len = PACKET_SIZE; } + + uint8_t * dst = &_buffers[next_slot].buf[0]; + void* buf = pbuf_get_contiguous(pb, dst, PACKET_SIZE, packet_len, 0); + if (buf) { + + if (buf != dst) + memcpy(dst, buf, packet_len); + _buffers[next_slot].len = packet_len; + + _buffers[next_slot].srcaddr = srcaddr; + _buffers[next_slot].dstaddr = ip_current_dest_addr(); + _buffers[next_slot].srcport = srcport; + _buffers[next_slot].input_netif = ip_current_input_netif(); + _udp_packets++; // we have one packet ready + } + pbuf_free(pb); // free memory immediately + } + + static void _s_recv(void *arg, + udp_pcb *upcb, pbuf *p, + CONST ip_addr_t *srcaddr, u16_t srcport) + { + reinterpret_cast(arg)->_recv(upcb, p, srcaddr, srcport); + } + +private: + udp_pcb* _pcb; + uint8_t _packet_number; + + UdpPacket * _buffers; + + // how many packets are ready. + int8_t _udp_packets; // number of udp packets ready to consume + bool _udp_ready; // is a packet currenlty consumed after a call to next() + // ring buffer ranges from 0..(_packet_number-1) + int8_t _udp_index; // current index in the ring buffer +}; + +#endif // ESP8266 +#endif //UDPMULTICASTLISTENER_H \ No newline at end of file diff --git a/tasmota/support_udp.ino b/tasmota/support_udp.ino index 451135e6a..cccb64b4e 100644 --- a/tasmota/support_udp.ino +++ b/tasmota/support_udp.ino @@ -19,7 +19,9 @@ #ifdef USE_EMULATION -#define UDP_BUFFER_SIZE 200 // Max UDP buffer size needed for M-SEARCH message +#ifndef UDP_BUFFER_SIZE +#define UDP_BUFFER_SIZE 120 // Max UDP buffer size needed for M-SEARCH message +#endif #define UDP_MSEARCH_SEND_DELAY 1500 // Delay in ms before M-Search response is send #include @@ -31,6 +33,15 @@ uint16_t udp_remote_port; // M-Search remote port bool udp_connected = false; bool udp_response_mutex = false; // M-Search response mutex to control re-entry +#ifdef ESP8266 +#ifndef UDP_MAX_PACKETS +#define UDP_MAX_PACKETS 3 // we support x more packets than the current one +#endif + +#include "UdpListener.h" +UdpListener UdpCtx(UDP_MAX_PACKETS); +#endif + /*********************************************************************************************\ * UPNP/SSDP search targets \*********************************************************************************************/ @@ -48,10 +59,16 @@ const char SSDP_ALL[] PROGMEM = "ssdp:all"; bool UdpDisconnect(void) { if (udp_connected) { + // flush any outgoing packet PortUdp.flush(); +#ifdef ESP8266 + UdpCtx.disconnect(); +#endif #ifdef USE_DEVICE_GROUPS + // stop PortUdp.stop(); #else // USE_DEVICE_GROUPS + // stop all WiFiUDP::stopAll(); #endif // !USE_DEVICE_GROUPS AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_UPNP D_MULTICAST_DISABLED)); @@ -64,13 +81,25 @@ bool UdpConnect(void) { if (!udp_connected && !restart_flag) { // Simple Service Discovery Protocol (SSDP) +#ifdef ESP8266 + UdpCtx.reset(); + if (igmp_joingroup(WiFi.localIP(), IPAddress(239,255,255,250)) == ERR_OK) { // addr 239.255.255.250 + ip_addr_t addr = IPADDR4_INIT(INADDR_ANY); + if (UdpCtx.listen(&addr, 1900)) { // port 1900 + // OK + AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPNP D_MULTICAST_REJOINED)); + udp_response_mutex = false; + udp_connected = true; + } +#else // ESP32 if (PortUdp.beginMulticast(WiFi.localIP(), IPAddress(239,255,255,250), 1900)) { AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPNP D_MULTICAST_REJOINED)); udp_response_mutex = false; udp_connected = true; - } else { +#endif + } + if (!udp_connected) { // if connection failed AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPNP D_MULTICAST_JOIN_FAILED)); - udp_connected = false; } } return udp_connected; @@ -79,14 +108,25 @@ bool UdpConnect(void) void PollUdp(void) { if (udp_connected) { +#ifdef ESP8266 + while (UdpCtx.next()) { + UdpPacket *packet; + packet = UdpCtx.read(); + if (packet->len >= UDP_BUFFER_SIZE) { + packet->len--; // leave space for NULL terminator + } + packet->buf[packet->len] = 0; // add NULL at the end of the packer + char * packet_buffer = (char*) &packet->buf; + int32_t len = packet->len; +#else // ESP32 while (PortUdp.parsePacket()) { char packet_buffer[UDP_BUFFER_SIZE]; // buffer to hold incoming UDP/SSDP packet - int len = PortUdp.read(packet_buffer, UDP_BUFFER_SIZE -1); + int32_t len = PortUdp.read(packet_buffer, UDP_BUFFER_SIZE -1); packet_buffer[len] = 0; - +#endif AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: Packet (%d)"), len); -// AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("\n%s"), packet_buffer); + // AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("\n%s"), packet_buffer); // Simple Service Discovery Protocol (SSDP) if (Settings.flag2.emulation) { @@ -97,11 +137,16 @@ void PollUdp(void) #endif udp_response_mutex = true; +#ifdef ESP8266 + udp_remote_ip = packet->srcaddr; + udp_remote_port = packet->srcport; +#else udp_remote_ip = PortUdp.remoteIP(); udp_remote_port = PortUdp.remotePort(); +#endif -// AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: M-SEARCH Packet from %s:%d\n%s"), -// udp_remote_ip.toString().c_str(), udp_remote_port, packet_buffer); + // AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: M-SEARCH Packet from %s:%d\n%s"), + // udp_remote_ip.toString().c_str(), udp_remote_port, packet_buffer); uint32_t response_delay = UDP_MSEARCH_SEND_DELAY + ((millis() &0x7) * 100); // 1500 - 2200 msec