diff --git a/code/espurna/config/arduino.h b/code/espurna/config/arduino.h index e54f6d5c..933aff06 100644 --- a/code/espurna/config/arduino.h +++ b/code/espurna/config/arduino.h @@ -78,6 +78,7 @@ //#define NTP_SUPPORT 0 //#define RF_SUPPORT 1 //#define SPIFFS_SUPPORT 1 +//#define SSDP_SUPPORT 1 //#define TELNET_SUPPORT 0 //#define TERMINAL_SUPPORT 0 //#define WEB_SUPPORT 0 diff --git a/code/espurna/config/general.h b/code/espurna/config/general.h index 672f88bb..091c6570 100644 --- a/code/espurna/config/general.h +++ b/code/espurna/config/general.h @@ -322,7 +322,7 @@ PROGMEM const char* const custom_reset_string[] = { #define API_REAL_TIME_VALUES 0 // Show filtered/median values by default (0 => median, 1 => real time) // ----------------------------------------------------------------------------- -// MDNS & LLMNR +// MDNS / LLMNR / NETBIOS / SSDP // ----------------------------------------------------------------------------- #ifndef MDNS_SUPPORT @@ -337,6 +337,10 @@ PROGMEM const char* const custom_reset_string[] = { #define NETBIOS_SUPPORT 0 // Publish device using NetBIOS protocol by default (1.26Kb) - requires 2.4.0 #endif +#ifndef SSDP_SUPPORT +#define SSDP_SUPPORT 0 // Publish device using SSDP protocol by default (3.32Kb) +#endif + // ----------------------------------------------------------------------------- // SPIFFS // ----------------------------------------------------------------------------- diff --git a/code/espurna/espurna.ino b/code/espurna/espurna.ino index fdcfdd86..e7aba73e 100644 --- a/code/espurna/espurna.ino +++ b/code/espurna/espurna.ino @@ -200,6 +200,9 @@ void welcome() { #if SPIFFS_SUPPORT DEBUG_MSG_P(PSTR(" SPIFFS")); #endif + #if SSDP_SUPPORT + DEBUG_MSG_P(PSTR(" SSDP")); + #endif #if TELNET_SUPPORT DEBUG_MSG_P(PSTR(" TELNET")); #endif @@ -293,6 +296,9 @@ void setup() { #if NETBIOS_SUPPORT netbiosSetup(); #endif + #if SSDP_SUPPORT + ssdpSetup(); + #endif #if NTP_SUPPORT ntpSetup(); #endif @@ -375,6 +381,9 @@ void loop() { #if POWER_PROVIDER != POWER_PROVIDER_NONE powerLoop(); #endif + #if SSDP_SUPPORT + ssdpLoop(); + #endif #if NTP_SUPPORT ntpLoop(); #endif diff --git a/code/espurna/libs/SSDPDevice.cpp b/code/espurna/libs/SSDPDevice.cpp new file mode 100644 index 00000000..b628a4ad --- /dev/null +++ b/code/espurna/libs/SSDPDevice.cpp @@ -0,0 +1,376 @@ +// +// +// + +#include "SSDPDevice.h" +#include "lwip/igmp.h" + +SSDPDeviceClass::SSDPDeviceClass() : + m_server(0), + m_port(80), + m_ttl(SSDP_MULTICAST_TTL) +{ + m_uuid[0] = '\0'; + m_modelNumber[0] = '\0'; + sprintf(m_deviceType, "urn:schemas-upnp-org:device:Basic:1"); + m_friendlyName[0] = '\0'; + m_presentationURL[0] = '\0'; + m_serialNumber[0] = '\0'; + m_modelName[0] = '\0'; + m_modelURL[0] = '\0'; + m_manufacturer[0] = '\0'; + m_manufacturerURL[0] = '\0'; + sprintf(m_schemaURL, "ssdp/schema.xml"); + + uint32_t chipId = ESP.getChipId(); + + sprintf(m_uuid, "38323636-4558-4dda-9188-cda0e6%02x%02x%02x", + (uint16_t)((chipId >> 16) & 0xff), + (uint16_t)((chipId >> 8) & 0xff), + (uint16_t)chipId & 0xff); + + for (int i = 0; i < SSDP_QUEUE_SIZE; i++) { + m_queue[i].time = 0; + } +} + +void SSDPDeviceClass::update() { + postNotifyUpdate(); +} + +bool SSDPDeviceClass::readLine(String &value) { + char buffer[65]; + int bufferPos = 0; + + while (1) { + int c = m_server->read(); + + if (c < 0) { + buffer[bufferPos] = '\0'; + + break; + } + if (c == '\r' && m_server->peek() == '\n') { + m_server->read(); + + buffer[bufferPos] = '\0'; + + break; + } + if (bufferPos < 64) { + buffer[bufferPos++] = c; + } + } + + value = String(buffer); + + return bufferPos > 0; +} + +bool SSDPDeviceClass::readKeyValue(String &key, String &value) { + char buffer[65]; + int bufferPos = 0; + + while (1) { + int c = m_server->read(); + + if (c < 0) { + if (bufferPos == 0) return false; + + buffer[bufferPos] = '\0'; + + break; + } + if (c == ':') { + buffer[bufferPos] = '\0'; + + while (m_server->peek() == ' ') m_server->read(); + + break; + } + else if (c == '\r' && m_server->peek() == '\n') { + m_server->read(); + + if (bufferPos == 0) return false; + + buffer[bufferPos] = '\0'; + + key = String(); + value = String(buffer); + + return true; + } + if (bufferPos < 64) { + buffer[bufferPos++] = c; + } + } + + key = String(buffer); + + readLine(value); + + return true; +} + +void SSDPDeviceClass::postNotifyALive() { + unsigned long time = millis(); + + post(NOTIFY_ALIVE_INIT, ROOT_FOR_ALL, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 10); + post(NOTIFY_ALIVE_INIT, ROOT_BY_UUID, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 55); + post(NOTIFY_ALIVE_INIT, ROOT_BY_TYPE, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 80); + + post(NOTIFY_ALIVE_INIT, ROOT_FOR_ALL, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 210); + post(NOTIFY_ALIVE_INIT, ROOT_BY_UUID, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 255); + post(NOTIFY_ALIVE_INIT, ROOT_BY_TYPE, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 280); + + post(NOTIFY_ALIVE, ROOT_FOR_ALL, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 610); + post(NOTIFY_ALIVE, ROOT_BY_UUID, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 655); + post(NOTIFY_ALIVE, ROOT_BY_TYPE, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 680); +} + +void SSDPDeviceClass::postNotifyUpdate() { + unsigned long time = millis(); + + post(NOTIFY_UPDATE, ROOT_FOR_ALL, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 10); + post(NOTIFY_UPDATE, ROOT_BY_UUID, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 55); + post(NOTIFY_UPDATE, ROOT_BY_TYPE, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 80); +} + +void SSDPDeviceClass::postResponse(long mx) { + unsigned long time = millis(); + unsigned long delay = random(0, mx) * 900L; // 1000 ms - 100 ms + + IPAddress address = m_server->remoteIP(); + uint16_t port = m_server->remotePort(); + + post(RESPONSE, ROOT_FOR_ALL, address, port, time + delay / 3); + post(RESPONSE, ROOT_BY_UUID, address, port, time + delay / 3 * 2); + post(RESPONSE, ROOT_BY_TYPE, address, port, time + delay); +} + +void SSDPDeviceClass::postResponse(ssdp_udn_t udn, long mx) { + post(RESPONSE, udn, m_server->remoteIP(), m_server->remotePort(), millis() + random(0, mx) * 900L); // 1000 ms - 100 ms +} + +void SSDPDeviceClass::post(ssdp_message_t type, ssdp_udn_t udn, IPAddress address, uint16_t port, unsigned long time) { + for (int i = 0; i < SSDP_QUEUE_SIZE; i++) { + if (m_queue[i].time == 0) { + m_queue[i].type = type; + m_queue[i].udn = udn; + m_queue[i].address = address; + m_queue[i].port = port; + m_queue[i].time = time; + + break; + } + } +} + +void SSDPDeviceClass::send(ssdp_send_parameters_t *parameters) { + char buffer[1460]; + unsigned int ip = WiFi.localIP(); + + const char *typeTemplate; + const char *uri, *usn1, *usn2, *usn3; + + switch (parameters->type) { + case NOTIFY_ALIVE_INIT: + case NOTIFY_ALIVE: + typeTemplate = SSDP_NOTIFY_ALIVE_TEMPLATE; + break; + case NOTIFY_UPDATE: + typeTemplate = SSDP_NOTIFY_UPDATE_TEMPLATE; + break; + default: // RESPONSE + typeTemplate = SSDP_RESPONSE_TEMPLATE; + break; + } + + String uuid = "uuid:" + String(m_uuid); + + switch (parameters->udn) { + case ROOT_FOR_ALL: + uri = "upnp:rootdevice"; + usn1 = uuid.c_str(); + usn2 = "::"; + usn3 = "upnp:rootdevice"; + break; + case ROOT_BY_UUID: + uri = uuid.c_str(); + usn1 = uuid.c_str(); + usn2 = ""; + usn3 = ""; + break; + case ROOT_BY_TYPE: + uri = m_deviceType; + usn1 = uuid.c_str(); + usn2 = "::"; + usn3 = m_deviceType; + break; + } + + int len = snprintf_P(buffer, sizeof(buffer), + SSDP_PACKET_TEMPLATE, typeTemplate, + SSDP_INTERVAL, m_modelName, m_modelNumber, usn1, usn2, usn3, parameters->type == RESPONSE ? "ST" : "NT", uri, + IP2STR(&ip), m_port, m_schemaURL + ); + + if (parameters->address == SSDP_MULTICAST_ADDR) { + m_server->beginPacketMulticast(parameters->address, parameters->port, m_ttl); + } + else { + m_server->beginPacket(parameters->address, parameters->port); + } + + m_server->write(buffer, len); + m_server->endPacket(); + + parameters->time = parameters->type == NOTIFY_ALIVE ? parameters->time + SSDP_INTERVAL * 900L : 0; // 1000 ms - 100 ms +} + +String SSDPDeviceClass::schema() { + char buffer[1024]; + uint32_t ip = WiFi.localIP(); + snprintf(buffer, sizeof(buffer), SSDP_SCHEMA_TEMPLATE, + IP2STR(&ip), m_port, m_schemaURL, + m_deviceType, + m_friendlyName, + m_presentationURL, + m_serialNumber, + m_modelName, + m_modelNumber, + m_modelURL, + m_manufacturer, + m_manufacturerURL, + m_uuid + ); + return String(buffer); +} + +void SSDPDeviceClass::handleClient() { + IPAddress current = WiFi.localIP(); + + if (m_last != current) { + m_last = current; + + for (int i = 0; i < SSDP_QUEUE_SIZE; i++) { + m_queue[i].time = 0; + } + + if (current != INADDR_NONE) { + if (!m_server) m_server = new WiFiUDP(); + + m_server->beginMulticast(current, SSDP_MULTICAST_ADDR, SSDP_PORT); + + postNotifyALive(); + } + else if (m_server) { + m_server->stop(); + } + } + + if (m_server && m_server->parsePacket()) { + String value; + + if (readLine(value) && value.equalsIgnoreCase("M-SEARCH * HTTP/1.1")) { + String key, st; + bool host = false, man = false; + long mx = 0; + + while (readKeyValue(key, value)) { + if (key.equalsIgnoreCase("HOST") && value.equals("239.255.255.250:1900")) { + host = true; + } + else if (key.equalsIgnoreCase("MAN") && value.equals("\"ssdp:discover\"")) { + man = true; + } + else if (key.equalsIgnoreCase("ST")) { + st = value; + } + else if (key.equalsIgnoreCase("MX")) { + mx = value.toInt(); + } + } + + if (host && man && mx > 0) { + if (st.equals("ssdp:all")) { + postResponse(mx); + } + else if (st.equals("upnp:rootdevice")) { + postResponse(ROOT_FOR_ALL, mx); + } + else if (st.equals("uuid:" + String(m_uuid))) { + postResponse(ROOT_BY_UUID, mx); + } + else if (st.equals(m_deviceType)) { + postResponse(ROOT_BY_TYPE, mx); + } + } + } + + m_server->flush(); + } + else { + unsigned long time = millis(); + + for (int i = 0; i < SSDP_QUEUE_SIZE; i++) { + if (m_queue[i].time > 0 && m_queue[i].time < time) { + send(&m_queue[i]); + } + } + } +} + +void SSDPDeviceClass::setSchemaURL(const char *url) { + strlcpy(m_schemaURL, url, sizeof(m_schemaURL)); +} + +void SSDPDeviceClass::setHTTPPort(uint16_t port) { + m_port = port; +} + +void SSDPDeviceClass::setDeviceType(const char *deviceType) { + strlcpy(m_deviceType, deviceType, sizeof(m_deviceType)); +} + +void SSDPDeviceClass::setName(const char *name) { + strlcpy(m_friendlyName, name, sizeof(m_friendlyName)); +} + +void SSDPDeviceClass::setURL(const char *url) { + strlcpy(m_presentationURL, url, sizeof(m_presentationURL)); +} + +void SSDPDeviceClass::setSerialNumber(const char *serialNumber) { + strlcpy(m_serialNumber, serialNumber, sizeof(m_serialNumber)); +} + +void SSDPDeviceClass::setSerialNumber(const uint32_t serialNumber) { + snprintf(m_serialNumber, sizeof(uint32_t) * 2 + 1, "%08X", serialNumber); +} + +void SSDPDeviceClass::setModelName(const char *name) { + strlcpy(m_modelName, name, sizeof(m_modelName)); +} + +void SSDPDeviceClass::setModelNumber(const char *num) { + strlcpy(m_modelNumber, num, sizeof(m_modelNumber)); +} + +void SSDPDeviceClass::setModelURL(const char *url) { + strlcpy(m_modelURL, url, sizeof(m_modelURL)); +} + +void SSDPDeviceClass::setManufacturer(const char *name) { + strlcpy(m_manufacturer, name, sizeof(m_manufacturer)); +} + +void SSDPDeviceClass::setManufacturerURL(const char *url) { + strlcpy(m_manufacturerURL, url, sizeof(m_manufacturerURL)); +} + +void SSDPDeviceClass::setTTL(const uint8_t ttl) { + m_ttl = ttl; +} + +SSDPDeviceClass SSDPDevice; diff --git a/code/espurna/libs/SSDPDevice.h b/code/espurna/libs/SSDPDevice.h new file mode 100644 index 00000000..d536fd98 --- /dev/null +++ b/code/espurna/libs/SSDPDevice.h @@ -0,0 +1,196 @@ +// SSDPDevice.h + +#ifndef _SSDPDEVICE_h +#define _SSDPDEVICE_h + +#if defined(ARDUINO) && ARDUINO >= 100 + #include "Arduino.h" +#else + #include "WProgram.h" +#endif + +#include +#include + +#define SSDP_INTERVAL 1200 +#define SSDP_PORT 1900 +//#define SSDP_METHOD_SIZE 10 +//#define SSDP_URI_SIZE 2 +//#define SSDP_BUFFER_SIZE 64 +#define SSDP_MULTICAST_TTL 2 + +#define SSDP_QUEUE_SIZE 21 + +static const IPAddress SSDP_MULTICAST_ADDR(239, 255, 255, 250); + +#define SSDP_UUID_SIZE 37 +#define SSDP_SCHEMA_URL_SIZE 64 +#define SSDP_DEVICE_TYPE_SIZE 64 +#define SSDP_FRIENDLY_NAME_SIZE 64 +#define SSDP_SERIAL_NUMBER_SIZE 32 +#define SSDP_PRESENTATION_URL_SIZE 128 +#define SSDP_MODEL_NAME_SIZE 64 +#define SSDP_MODEL_URL_SIZE 128 +#define SSDP_MODEL_VERSION_SIZE 32 +#define SSDP_MANUFACTURER_SIZE 64 +#define SSDP_MANUFACTURER_URL_SIZE 128 + +static const char* PROGMEM SSDP_RESPONSE_TEMPLATE = + "HTTP/1.1 200 OK\r\n" + "EXT:\r\n"; + +static const char* PROGMEM SSDP_NOTIFY_ALIVE_TEMPLATE = + "NOTIFY * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "NTS: ssdp:alive\r\n"; + +static const char* PROGMEM SSDP_NOTIFY_UPDATE_TEMPLATE = + "NOTIFY * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "NTS: ssdp:update\r\n"; + +static const char* PROGMEM SSDP_PACKET_TEMPLATE = + "%s" // _ssdp_response_template / _ssdp_notify_template + "CACHE-CONTROL: max-age=%u\r\n" // SSDP_INTERVAL + "SERVER: UPNP/1.1 %s/%s\r\n" // m_modelName, m_modelNumber + "USN: %s%s%s\r\n" // m_uuid + "%s: %s\r\n" // "NT" or "ST", m_deviceType + "LOCATION: http://%u.%u.%u.%u:%u/%s\r\n" // WiFi.localIP(), m_port, m_schemaURL + "\r\n"; + +static const char* PROGMEM SSDP_SCHEMA_TEMPLATE = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/xml\r\n" + "Connection: close\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n" + "" + "" + "" + "1" + "0" + "" + "http://%u.%u.%u.%u:%u/%s" // WiFi.localIP(), _port + "" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "uuid:%s" + "" +// "" +// "" +// "image/png" +// "48" +// "48" +// "24" +// "icon48.png" +// "" +// "" +// "image/png" +// "120" +// "120" +// "24" +// "icon120.png" +// "" +// "" + "\r\n" + "\r\n"; + +typedef enum { + NOTIFY_ALIVE_INIT, + NOTIFY_ALIVE, + NOTIFY_UPDATE, + RESPONSE +} ssdp_message_t; + +typedef enum { + ROOT_FOR_ALL, + ROOT_BY_UUID, + ROOT_BY_TYPE +} ssdp_udn_t; + +typedef struct { + unsigned long time; + + ssdp_message_t type; + ssdp_udn_t udn; + uint32_t address; + uint16_t port; +} ssdp_send_parameters_t; + +class SSDPDeviceClass { +private: + WiFiUDP *m_server; + + IPAddress m_last; + + char m_schemaURL[SSDP_SCHEMA_URL_SIZE]; + char m_uuid[SSDP_UUID_SIZE]; + char m_deviceType[SSDP_DEVICE_TYPE_SIZE]; + char m_friendlyName[SSDP_FRIENDLY_NAME_SIZE]; + char m_serialNumber[SSDP_SERIAL_NUMBER_SIZE]; + char m_presentationURL[SSDP_PRESENTATION_URL_SIZE]; + char m_manufacturer[SSDP_MANUFACTURER_SIZE]; + char m_manufacturerURL[SSDP_MANUFACTURER_URL_SIZE]; + char m_modelName[SSDP_MODEL_NAME_SIZE]; + char m_modelURL[SSDP_MODEL_URL_SIZE]; + char m_modelNumber[SSDP_MODEL_VERSION_SIZE]; + + uint16_t m_port; + uint8_t m_ttl; + + ssdp_send_parameters_t m_queue[SSDP_QUEUE_SIZE]; +protected: + bool readLine(String &value); + bool readKeyValue(String &key, String &value); + + void postNotifyALive(); + void postNotifyUpdate(); + void postResponse(long mx); + void postResponse(ssdp_udn_t udn, long mx); + void post(ssdp_message_t type, ssdp_udn_t udn, IPAddress address, uint16_t port, unsigned long time); + + void send(ssdp_send_parameters_t *parameters); +public: + SSDPDeviceClass(); + + void update(); + + String schema(); + + void handleClient(); + + void setDeviceType(const String& deviceType) { setDeviceType(deviceType.c_str()); } + void setDeviceType(const char *deviceType); + void setName(const String& name) { setName(name.c_str()); } + void setName(const char *name); + void setURL(const String& url) { setURL(url.c_str()); } + void setURL(const char *url); + void setSchemaURL(const String& url) { setSchemaURL(url.c_str()); } + void setSchemaURL(const char *url); + void setSerialNumber(const String& serialNumber) { setSerialNumber(serialNumber.c_str()); } + void setSerialNumber(const char *serialNumber); + void setSerialNumber(const uint32_t serialNumber); + void setModelName(const String& name) { setModelName(name.c_str()); } + void setModelName(const char *name); + void setModelNumber(const String& num) { setModelNumber(num.c_str()); } + void setModelNumber(const char *num); + void setModelURL(const String& url) { setModelURL(url.c_str()); } + void setModelURL(const char *url); + void setManufacturer(const String& name) { setManufacturer(name.c_str()); } + void setManufacturer(const char *name); + void setManufacturerURL(const String& url) { setManufacturerURL(url.c_str()); } + void setManufacturerURL(const char *url); + void setHTTPPort(uint16_t port); + void setTTL(uint8_t ttl); +}; + +extern SSDPDeviceClass SSDPDevice; + +#endif diff --git a/code/espurna/ssdp.ino b/code/espurna/ssdp.ino new file mode 100644 index 00000000..6424dbcd --- /dev/null +++ b/code/espurna/ssdp.ino @@ -0,0 +1,41 @@ +/* + +SSDP MODULE + +Copyright (C) 2017 by Xose PĂ©rez +Uses SSDP library by PawelDino (https://github.com/PawelDino) +https://github.com/esp8266/Arduino/issues/2283#issuecomment-299635604 + +*/ + +#if SSDP_SUPPORT + +#include + +void ssdpSetup() { + + SSDPDevice.setName(getSetting("hostname")); + SSDPDevice.setDeviceType("urn:schemas-upnp-org:device:BinaryLight:1"); + SSDPDevice.setSchemaURL("description.xml"); + SSDPDevice.setSerialNumber(ESP.getChipId()); + SSDPDevice.setURL("/"); + SSDPDevice.setModelName(DEVICE); + SSDPDevice.setModelNumber(""); + SSDPDevice.setManufacturer(MANUFACTURER); + + #if WEB_SUPPORT + webServer()->on("/description.xml", HTTP_GET, [](AsyncWebServerRequest *request) { + DEBUG_MSG_P(PSTR("[SSDP] Schema request\n")); + String schema = SSDPDevice.schema(); + Serial.println(schema); + request->send(200, "application/xml", schema.c_str()); + }); + #endif + +} + +void ssdpLoop() { + SSDPDevice.handleClient(); +} + +#endif // SSDP_SUPPORT