Merge pull request #8563 from s-hadinger/udplistener

reduce footprint of multicast udp listener
This commit is contained in:
Theo Arends 2020-05-28 09:34:35 +02:00 committed by GitHub
commit fdc5ffa36b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 269 additions and 8 deletions

View File

@ -0,0 +1,7 @@
name=UdpListener
version=1.0
author=Ivan Grokhotkov, Stephan Hadinger
maintainer=Stephan <stephan.hadinger@gmail.com>
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

View File

@ -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 <http://www.gnu.org/licenses/>.@
*/
// 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 <n> 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 <Arduino.h>
extern "C" {
#include <lwip/udp.h>
#include <lwip/igmp.h>
}
template <size_t PACKET_SIZE>
struct UdpPacket {
IPAddress srcaddr;
IPAddress dstaddr;
int16_t srcport;
netif* input_netif;
size_t len;
uint8_t buf[PACKET_SIZE];
};
template <size_t PACKET_SIZE>
class UdpListener
{
public:
typedef std::function<void(void)> 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_SIZE>[_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<PACKET_SIZE> * 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<UdpListener*>(arg)->_recv(upcb, p, srcaddr, srcport);
}
private:
udp_pcb* _pcb;
uint8_t _packet_number;
UdpPacket<PACKET_SIZE> * _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

View File

@ -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 <Ticker.h>
@ -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<UDP_BUFFER_SIZE> 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<UDP_BUFFER_SIZE> *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