diff --git a/Sming/Libraries/DIAL/.cs b/Sming/Libraries/DIAL/.cs
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Sming/Libraries/DIAL/README.rst b/Sming/Libraries/DIAL/README.rst
new file mode 100644
index 0000000000..0a331864a9
--- /dev/null
+++ b/Sming/Libraries/DIAL/README.rst
@@ -0,0 +1,8 @@
+DIAL protocol
+=============
+
+Introduction
+------------
+
+DIAL—for DIscovery And Launch—is a simple protocol that second-screen devices can use to discover and launch apps on first-screen devices.
+For example, your can stream a video from your embedded device on your connected TV.
\ No newline at end of file
diff --git a/Sming/Libraries/DIAL/component.mk b/Sming/Libraries/DIAL/component.mk
new file mode 100644
index 0000000000..70dd47ad52
--- /dev/null
+++ b/Sming/Libraries/DIAL/component.mk
@@ -0,0 +1,3 @@
+COMPONENT_SRCDIRS := src/Dial
+COMPONENT_INCDIRS := src/
+COMPONENT_DEPENDS := UPnP SSDP
diff --git a/Sming/Libraries/DIAL/sample/.project b/Sming/Libraries/DIAL/sample/.project
new file mode 100644
index 0000000000..8d9109cb8d
--- /dev/null
+++ b/Sming/Libraries/DIAL/sample/.project
@@ -0,0 +1,28 @@
+
+
+ DIAL_Client
+
+
+ SmingFramework
+
+
+
+ org.eclipse.cdt.managedbuilder.core.genmakebuilder
+ clean,full,incremental,
+
+
+
+
+ org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder
+ full,incremental,
+
+
+
+
+
+ org.eclipse.cdt.core.cnature
+ org.eclipse.cdt.core.ccnature
+ org.eclipse.cdt.managedbuilder.core.managedBuildNature
+ org.eclipse.cdt.managedbuilder.core.ScannerConfigNature
+
+
diff --git a/Sming/Libraries/DIAL/sample/Makefile b/Sming/Libraries/DIAL/sample/Makefile
new file mode 100644
index 0000000000..ff51b6c3a7
--- /dev/null
+++ b/Sming/Libraries/DIAL/sample/Makefile
@@ -0,0 +1,9 @@
+#####################################################################
+#### Please don't change this file. Use component.mk instead ####
+#####################################################################
+
+ifndef SMING_HOME
+$(error SMING_HOME is not set: please configure it as an environment variable)
+endif
+
+include $(SMING_HOME)/project.mk
diff --git a/Sming/Libraries/DIAL/sample/README.rst b/Sming/Libraries/DIAL/sample/README.rst
new file mode 100644
index 0000000000..765c746f5d
--- /dev/null
+++ b/Sming/Libraries/DIAL/sample/README.rst
@@ -0,0 +1,4 @@
+Control your DIAL-enabled smart monitor/TV using Sming
+======================================================
+
+Demonstrates starting of YouTube, playing a video and closing YouTube after a small period of time.
diff --git a/Sming/Libraries/DIAL/sample/app/application.cpp b/Sming/Libraries/DIAL/sample/app/application.cpp
new file mode 100644
index 0000000000..a04c0f5d37
--- /dev/null
+++ b/Sming/Libraries/DIAL/sample/app/application.cpp
@@ -0,0 +1,92 @@
+#include
+#include
+
+// If you want, you can define WiFi settings globally in Eclipse Environment Variables
+#ifndef WIFI_SSID
+#define WIFI_SSID "PleaseEnterSSID" // Put you SSID and Password here
+#define WIFI_PWD "PleaseEnterPass"
+#endif
+
+Dial::Client client;
+
+void onRun(Dial::App& app, HttpResponse& response)
+{
+ auto responseCode = response.code;
+ if(responseCode < 400) {
+ auto timer = new AutoDeleteTimer;
+ timer->initializeMs<20000>([&]() {
+ // Once started the app can also be stopped using the command below
+ Serial.printf(_F("Stopping application: %s\n"), app.getName().c_str());
+ app.stop();
+ });
+ timer->startOnce();
+ }
+}
+
+void onStatus(Dial::App& app, HttpResponse& response)
+{
+ auto responseCode = response.code;
+ if(responseCode == HTTP_STATUS_NOT_FOUND) {
+ Serial.printf(_F("Unable to find the desired application: %s\n"), app.getName().c_str());
+ return;
+ }
+
+ HttpParams params;
+ params["v"] = "fC9HdQUaFtA";
+ app.run(params, onRun);
+}
+
+void onConnected(Dial::Client& client, const XML::Document& doc, const HttpHeaders& headers)
+{
+ CStringArray path("device");
+ path.add("friendlyName");
+ auto node = client.getNode(doc, path);
+ Serial.println(_F("New DIAL device found."));
+ if(node != nullptr) {
+ Serial.printf(_F("Friendly name: %s.\n"), node->value());
+ }
+
+ auto app = client.getApp("YouTube");
+ app->status(onStatus);
+}
+
+void connectOk(IpAddress ip, IpAddress mask, IpAddress gateway)
+{
+ Serial.print(_F("I'm CONNECTED to "));
+ Serial.println(ip);
+
+ /* The command below will use UPnP to auto-discover a smart monitor/TV */
+ client.connect(onConnected);
+
+ /* Alternatevely one can use the commands below when auto-discovery is not working */
+ /*
+ Url descriptionUrl{"192.168.22.222:55000/nrc/ddd.xml"};
+
+ client.connect(descriptionUrl, onConnected);
+ */
+}
+
+void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reason)
+{
+ // The different reason codes can be found in user_interface.h. in your SDK.
+ Serial.print(_F("Disconnected from \""));
+ Serial.print(ssid);
+ Serial.print(_F("\", reason: "));
+ Serial.println(WifiEvents.getDisconnectReasonDesc(reason));
+}
+
+void init()
+{
+ Serial.begin(SERIAL_BAUD_RATE);
+ Serial.systemDebugOutput(true); // Allow debug print to serial
+
+ // Station - WiFi client
+ WifiStation.enable(true);
+ WifiStation.config(_F(WIFI_SSID), _F(WIFI_PWD));
+
+ // Set callback that should be triggered when we have assigned IP
+ WifiEvents.onStationGotIP(connectOk);
+
+ // Set callback that should be triggered if we are disconnected or connection attempt failed
+ WifiEvents.onStationDisconnect(connectFail);
+}
diff --git a/Sming/Libraries/DIAL/sample/component.mk b/Sming/Libraries/DIAL/sample/component.mk
new file mode 100644
index 0000000000..d860cf96b9
--- /dev/null
+++ b/Sming/Libraries/DIAL/sample/component.mk
@@ -0,0 +1,3 @@
+ARDUINO_LIBRARIES := DIAL
+
+DISABLE_SPIFFS = 1
diff --git a/Sming/Libraries/DIAL/src/Dial/App.cpp b/Sming/Libraries/DIAL/src/Dial/App.cpp
new file mode 100644
index 0000000000..2be5587edb
--- /dev/null
+++ b/Sming/Libraries/DIAL/src/Dial/App.cpp
@@ -0,0 +1,112 @@
+#include "App.h"
+
+namespace Dial
+{
+HttpClient App::http;
+
+bool App::status(ResponseCallback onResponse)
+{
+ auto request = new HttpRequest(applicationUrl);
+ request->method = HTTP_GET;
+ request->setResponseStream(new LimitedMemoryStream(maxDescriptionSize));
+ if(onResponse != nullptr) {
+ request->onRequestComplete([onResponse, this](HttpConnection& connection, bool successful) -> int {
+ onResponse(*this, *(connection.getResponse()));
+
+ return 0;
+ });
+ }
+
+ return http.send(request);
+}
+
+bool App::run(ResponseCallback onResponse)
+{
+ auto request = new HttpRequest(applicationUrl);
+ request->method = HTTP_POST;
+ request->setResponseStream(new LimitedMemoryStream(maxDescriptionSize));
+ request->onRequestComplete([onResponse, this](HttpConnection& connection, bool successful) -> int {
+ auto headers = connection.getResponse()->headers;
+ if(headers.contains(HTTP_HEADER_LOCATION)) {
+ this->instanceUrl = headers[HTTP_HEADER_LOCATION];
+ }
+
+ if(onResponse != nullptr) {
+ onResponse(*this, *(connection.getResponse()));
+ }
+
+ return 0;
+ });
+
+ return http.send(request);
+}
+
+bool App::run(const String& body, enum MimeType mime, ResponseCallback onResponse)
+{
+ auto request = new HttpRequest(applicationUrl);
+ request->method = HTTP_POST;
+ request->headers[HTTP_HEADER_CONTENT_TYPE] = ContentType::toString(mime);
+ if(body.length() != 0) {
+ request->setBody(body);
+ }
+ request->setResponseStream(new LimitedMemoryStream(maxDescriptionSize));
+ request->onRequestComplete([onResponse, this](HttpConnection& connection, bool successful) -> int {
+ auto headers = connection.getResponse()->headers;
+ if(headers.contains(HTTP_HEADER_LOCATION)) {
+ this->instanceUrl = headers[HTTP_HEADER_LOCATION];
+ }
+
+ if(onResponse != nullptr) {
+ onResponse(*this, *(connection.getResponse()));
+ }
+
+ return 0;
+ });
+
+ return http.send(request);
+}
+
+bool App::run(const HttpParams& params, ResponseCallback onResponse)
+{
+ auto request = new HttpRequest(applicationUrl);
+ request->method = HTTP_POST;
+ request->setPostParameters(params);
+ request->setResponseStream(new LimitedMemoryStream(maxDescriptionSize));
+ request->onRequestComplete([onResponse, this](HttpConnection& connection, bool successful) -> int {
+ auto headers = connection.getResponse()->headers;
+ if(headers.contains(HTTP_HEADER_LOCATION)) {
+ this->instanceUrl = headers[HTTP_HEADER_LOCATION];
+ }
+
+ if(onResponse != nullptr) {
+ onResponse(*this, *(connection.getResponse()));
+ }
+
+ return 0;
+ });
+
+ return http.send(request);
+}
+
+bool App::stop(ResponseCallback onResponse)
+{
+ if(instanceUrl.length() == 0) {
+ debug_w("Instance URL not set. Only started apps can be stopped.");
+ return false;
+ }
+
+ auto request = new HttpRequest(Url(instanceUrl));
+ request->method = HTTP_DELETE;
+ request->setResponseStream(new LimitedMemoryStream(maxDescriptionSize));
+ if(onResponse != nullptr) {
+ request->onRequestComplete([onResponse, this](HttpConnection& connection, bool successful) -> int {
+ onResponse(*this, *(connection.getResponse()));
+
+ return 0;
+ });
+ }
+
+ return http.send(request);
+}
+
+} // namespace Dial
diff --git a/Sming/Libraries/DIAL/src/Dial/App.h b/Sming/Libraries/DIAL/src/Dial/App.h
new file mode 100644
index 0000000000..568046e75b
--- /dev/null
+++ b/Sming/Libraries/DIAL/src/Dial/App.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include
+
+namespace Dial
+{
+class App
+{
+public:
+ using ResponseCallback = Delegate;
+
+ App(const String& name, const Url& appsUrl, size_t maxDescriptionSize = 2048)
+ : name(name), maxDescriptionSize(maxDescriptionSize)
+ {
+ applicationUrl = Url(appsUrl.toString() + '/' + name);
+ instanceUrl = applicationUrl;
+ }
+
+ String getName()
+ {
+ return name;
+ }
+
+ bool status(ResponseCallback onResponse);
+
+ /**
+ *
+ */
+ bool run(ResponseCallback onResponse = nullptr);
+
+ bool run(const String& body, enum MimeType mime, ResponseCallback onResponse = nullptr);
+
+ bool run(const HttpParams& params, ResponseCallback onResponse = nullptr);
+
+ bool stop(ResponseCallback onResponse = nullptr);
+
+private:
+ String name;
+ Url applicationUrl;
+ String instanceUrl;
+ size_t maxDescriptionSize;
+ static HttpClient http;
+};
+
+} // namespace Dial
diff --git a/Sming/Libraries/DIAL/src/Dial/Client.cpp b/Sming/Libraries/DIAL/src/Dial/Client.cpp
new file mode 100644
index 0000000000..f4a169bf96
--- /dev/null
+++ b/Sming/Libraries/DIAL/src/Dial/Client.cpp
@@ -0,0 +1,158 @@
+#include "Client.h"
+
+#include
+#include
+
+namespace Dial
+{
+HttpClient Client::http;
+
+bool Client::formatMessage(SSDP::Message& message, SSDP::MessageSpec& ms)
+{
+ // Override the search target
+ message["ST"] = searchType;
+ return true;
+}
+
+void Client::onNotify(SSDP::BasicMessage& message)
+{
+ if(searchType != message["NT"] && searchType != message["ST"]) {
+ return;
+ }
+
+ auto location = message[HTTP_HEADER_LOCATION];
+ if(location == nullptr) {
+ debug_d("No valid Location header found.");
+ return;
+ }
+
+ auto uniqueServiceName = message["USN"];
+ if(uniqueServiceName == nullptr) {
+ debug_d("No valid USN header found.");
+ return;
+ }
+
+ if(uniqueServiceNames.contains(uniqueServiceName)) {
+ return; // Already found
+ }
+ uniqueServiceNames += uniqueServiceName;
+
+ debug_d("Fetching description from URL: '%s'", location);
+ Url url(location);
+ auto request = new HttpRequest(url);
+ request->setResponseStream(new LimitedMemoryStream(maxDescriptionSize));
+ request->onRequestComplete(RequestCompletedDelegate(&Client::onDescription, this));
+ http.send(request);
+}
+
+int Client::onDescription(HttpConnection& conn, bool success)
+{
+ if(!success) {
+ debug_e("Fetch failed");
+ return 0;
+ }
+
+ debug_i("Received description");
+ auto response = conn.getResponse();
+ if(response->stream == nullptr) {
+ debug_e("No body");
+ return 0;
+ }
+
+ if(response->headers.contains(_F("Application-URL"))) {
+ applicationUrl = response->headers[_F("Application-URL")];
+ }
+
+ auto stream = reinterpret_cast(response->stream);
+ stream->print('\0');
+ XML::Document doc;
+ XML::deserialize(doc, stream->getStreamPointer());
+
+ descriptionUrl = Url(conn.getRequest()->uri);
+
+ debug_d("Found DIAL device with searchType: %s", searchType.c_str());
+ if(onConnected) {
+ onConnected(*this, doc, response->headers);
+ }
+
+ return 0;
+}
+
+bool Client::connect(ConnectedCallback callback, const String& type)
+{
+ if(!UPnP::deviceHost.begin()) {
+ debug_e("UPnP initialisation failed");
+ return false;
+ }
+
+ onConnected = callback;
+ searchType = type;
+
+ UPnP::deviceHost.registerControlPoint(this);
+
+ auto message = new SSDP::MessageSpec(SSDP::MESSAGE_MSEARCH);
+ message->object = this;
+ message->repeat = 2;
+ message->target = SSDP::TARGET_ROOT;
+ SSDP::server.messageQueue.add(message, 0);
+
+ return true;
+}
+
+bool Client::connect(const Url& descriptionUrl, ConnectedCallback callback)
+{
+ this->descriptionUrl = descriptionUrl;
+ onConnected = callback;
+
+ debug_d("Fetching '%s'", descriptionUrl.toString().c_str());
+ auto request = new HttpRequest(descriptionUrl);
+ request->setResponseStream(new LimitedMemoryStream(maxDescriptionSize));
+ request->onRequestComplete(RequestCompletedDelegate(&Client::onDescription, this));
+ http.send(request);
+
+ return true;
+}
+
+App* Client::getApp(const String& applicationId)
+{
+ if(apps.contains(applicationId)) {
+ return apps[applicationId];
+ }
+
+ apps[applicationId] = new App(applicationId, applicationUrl);
+
+ return apps[applicationId];
+}
+
+XML::Node* Client::getNode(HttpConnection& connection, const CStringArray& path)
+{
+ HttpResponse* response = connection.getResponse();
+ if(response->stream == nullptr) {
+ debug_e("No body");
+ return nullptr;
+ }
+
+ auto stream = reinterpret_cast(response->stream);
+ stream->print('\0');
+ XML::Document doc;
+ XML::deserialize(doc, stream->getStreamPointer());
+
+ return getNode(doc, path);
+}
+
+XML::Node* Client::getNode(const XML::Document& doc, const CStringArray& path)
+{
+ auto node = doc.first_node();
+ if(node != nullptr) {
+ for(size_t i = 0; i < path.count(); i++) {
+ node = node->first_node(path[i]);
+ if(node == nullptr) {
+ break;
+ }
+ }
+ }
+
+ return node;
+}
+
+} // namespace Dial
diff --git a/Sming/Libraries/DIAL/src/Dial/Client.h b/Sming/Libraries/DIAL/src/Dial/Client.h
new file mode 100644
index 0000000000..30d0d00812
--- /dev/null
+++ b/Sming/Libraries/DIAL/src/Dial/Client.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include
+#include
+#include
+#include "App.h"
+
+using namespace rapidxml;
+
+namespace Dial
+{
+class Client : public UPnP::ControlPoint
+{
+public:
+ using ConnectedCallback = Delegate;
+
+ Client(size_t maxDescriptionSize = 4096) : maxDescriptionSize(maxDescriptionSize)
+ {
+ }
+
+ /**
+ * @brief Searches for a DIAL device identified by a search type
+ * @param callback will be called once such a device is auto-discovered
+ * @param urn unique identifier of the search type
+ *
+ * @retval true when the connect request can be started
+ */
+ virtual bool connect(ConnectedCallback callback, const String& type = "urn:dial-multiscreen-org:service:dial:1");
+
+ /**
+ * @brief Directly connects to a device's description xml URL.
+ * @param descriptionUrl the full URL where a description XML can be found.
+ * For example: http://192.168.22.222:55000/nrc/ddd.xml";
+ * @param callback will be called once the XML is fetched
+ *
+ * @retval true when the connect request can be started
+ */
+ bool connect(const Url& descriptionUrl, ConnectedCallback callback);
+
+ bool formatMessage(SSDP::Message& msg, SSDP::MessageSpec& ms) override;
+
+ void onNotify(SSDP::BasicMessage& msg) override;
+
+ App* getApp(const String& applicationId);
+
+ /**
+ * TODO: Move this method to XML::Document ...
+ *
+ * @brief Gets XML node by path
+ * @param doc the XML document
+ * @param path the paths that have to be traversed to get the node (excluding the root node).
+ *
+ * @retval node
+ *
+ */
+ XML::Node* getNode(const XML::Document& doc, const CStringArray& path);
+
+protected:
+ static HttpClient http;
+
+ Url getDescriptionUrl()
+ {
+ return descriptionUrl;
+ }
+
+ XML::Node* getNode(HttpConnection& connection, const CStringArray& path);
+
+private:
+ size_t maxDescriptionSize; // <<< Maximum size of TV XML description that is stored.
+ ConnectedCallback onConnected;
+
+ Url descriptionUrl;
+ Url applicationUrl;
+ String searchType;
+ CStringArray uniqueServiceNames;
+ ObjectMap apps; // <<< list of invoked apps
+
+ int onDescription(HttpConnection& conn, bool success);
+};
+
+} // namespace Dial
diff --git a/Sming/Libraries/Panasonic-Viera/component.mk b/Sming/Libraries/Panasonic-Viera/component.mk
new file mode 100644
index 0000000000..324d5bddad
--- /dev/null
+++ b/Sming/Libraries/Panasonic-Viera/component.mk
@@ -0,0 +1,3 @@
+COMPONENT_SRCDIRS := src/Panasonic/VieraTV
+COMPONENT_INCDIRS := src/
+COMPONENT_DEPENDS := DIAL
\ No newline at end of file
diff --git a/Sming/Libraries/Panasonic-Viera/docs/application_codes.md b/Sming/Libraries/Panasonic-Viera/docs/application_codes.md
new file mode 100644
index 0000000000..3e834768e0
--- /dev/null
+++ b/Sming/Libraries/Panasonic-Viera/docs/application_codes.md
@@ -0,0 +1,42 @@
+Thanks to: https://raw.githubusercontent.com/g30r93g/homebridge-panasonic/master/README.md
+
+Or this one:
+https://forums.indigodomo.com/viewtopic.php?f=134&t=17875&start=15
+
+## App List
+
+This is a partial list of apps that are on Viera TV's. Make sure that the app exists on your TV.
+
+|App Name|App ID|
+|:---|:---------------:|
+|Netflix|`0010000200000001`|
+|YouTube|`0070000200170001`|
+|Amazon Prime Video|`0010000100170001`|
+|Plex|`0076010507000001`|
+|BBC iPlayer|`0020000A00170010`|
+|BBC News|`0020000A00170006`|
+|BBC Sport|`0020000A00170007`|
+|ITV Hub|`0387878700000124`|
+|TuneIn|`0010001800000001`|
+|AccuWeather|`0070000C00000001`|
+|All 4|`0387878700000125`|
+|Demand 5|`0020009300000002`|
+|Rakuten TV|`0020002A00000001`|
+|CHILI|`0020004700000001`|
+|STV Player|`0387878700000132`|
+|Digital Concert Hall|`0076002307170001`|
+|Apps Market|`0387878700000102`|
+|Browser|`0077777700160002`|
+|Calendar|`0387878700150020`|
+|VIERA Link|`0387878700000016`|
+|Recorded TV|`0387878700000013`|
+|Freeview Catch Up|`0387878700000109`|
+
+If you want to find the App ID for an app yourself, follow these steps:
+
+1. Install Wireshark
+2. Install Panasonic TV Remote on your mobile device
+3. Use your mobile device as a network interface in Wireshark
+4. Use the Panasonic TV Remote app launcher to open the application you want
+5. Filter results by 'http' and find the request to open YouTube (It will be in the XML with a tag called `X_LaunchApp`)
+6. Find the app ID. It will look something like `product_id=000000000`. `product_id` is the app ID
\ No newline at end of file
diff --git a/Sming/Libraries/Panasonic-Viera/sample/app/application.cpp b/Sming/Libraries/Panasonic-Viera/sample/app/application.cpp
new file mode 100644
index 0000000000..5a40a1c479
--- /dev/null
+++ b/Sming/Libraries/Panasonic-Viera/sample/app/application.cpp
@@ -0,0 +1,71 @@
+#include
+#include
+
+// If you want, you can define WiFi settings globally in Eclipse Environment Variables
+#ifndef WIFI_SSID
+#define WIFI_SSID "PleaseEnterSSID" // Put you SSID and Password here
+#define WIFI_PWD "PleaseEnterPass"
+#endif
+
+using namespace Panasonic;
+
+VieraTV::Client client;
+
+void onConnected(Dial::Client& dialClient, const XML::Document& doc, const HttpHeaders& headers)
+{
+ CStringArray path("device");
+ path.add("friendlyName");
+ auto node = client.getNode(doc, path);
+ Serial.println(_F("New Viera TV found."));
+ if(node != nullptr) {
+ Serial.printf(_F("Friendly name: %s.\n"), node->value());
+ }
+
+ client.setMute(true); // mute the TV
+ client.getMute([](bool muted) -> void { // check the mute state
+ Serial.printf("Muted state: %d", muted ? 1 : 0);
+ });
+ client.sendCommand(VieraTV::Action::ACTION_CH_UP);
+ client.launch(VieraTV::ApplicationId::APP_YOUTUBE);
+}
+
+void connectOk(IpAddress ip, IpAddress mask, IpAddress gateway)
+{
+ Serial.print(_F("I'm CONNECTED to "));
+ Serial.println(ip);
+
+ /* The command below will use UPnP to auto-discover a Viera TV */
+ client.connect(onConnected);
+
+ /* Alternatevely one can use the commands below when auto-discovery is not working */
+ /*
+ Url descriptionUrl{"192.168.22.222:55000/nrc/ddd.xml"};
+
+ client.connect(descriptionUrl, onConnected);
+ */
+}
+
+void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reason)
+{
+ // The different reason codes can be found in user_interface.h. in your SDK.
+ Serial.print(_F("Disconnected from \""));
+ Serial.print(ssid);
+ Serial.print(_F("\", reason: "));
+ Serial.println(WifiEvents.getDisconnectReasonDesc(reason));
+}
+
+void init()
+{
+ Serial.begin(SERIAL_BAUD_RATE);
+ Serial.systemDebugOutput(true); // Allow debug print to serial
+
+ // Station - WiFi client
+ WifiStation.enable(true);
+ WifiStation.config(_F(WIFI_SSID), _F(WIFI_PWD));
+
+ // Set callback that should be triggered when we have assigned IP
+ WifiEvents.onStationGotIP(connectOk);
+
+ // Set callback that should be triggered if we are disconnected or connection attempt failed
+ WifiEvents.onStationDisconnect(connectFail);
+}
diff --git a/Sming/Libraries/Panasonic-Viera/src/Panasonic/VieraTV/Client.cpp b/Sming/Libraries/Panasonic-Viera/src/Panasonic/VieraTV/Client.cpp
new file mode 100644
index 0000000000..34db0a24ca
--- /dev/null
+++ b/Sming/Libraries/Panasonic-Viera/src/Panasonic/VieraTV/Client.cpp
@@ -0,0 +1,217 @@
+#include "Client.h"
+#include
+
+namespace Panasonic
+{
+namespace VieraTV
+{
+#define XX(action, description) #action "\0"
+DEFINE_FSTR_LOCAL(vieraCommands, VIERA_COMMAND_MAP(XX))
+#undef XX
+
+#define XX(id, name, code) #code "\0"
+DEFINE_FSTR_LOCAL(vieraApps, VIERA_APP_MAP(XX))
+#undef XX
+
+String toString(enum Action a)
+{
+ return CStringArray(vieraCommands)[(int)a];
+}
+
+String toString(enum ApplicationId a)
+{
+ return CStringArray(vieraApps)[(int)a];
+}
+
+bool Client::sendCommand(Action action)
+{
+ Command cmd;
+ cmd.type = Command::Type::REMOTE;
+ cmd.name = F("X_SendKey");
+
+ String text = F("NRC_");
+ text += toString(action);
+ text += F("-ONOFF");
+
+ setParams(cmd, text);
+
+ return sendRequest(cmd);
+}
+
+bool Client::switchToHdmi(size_t input)
+{
+ Command cmd;
+ cmd.type = Command::Type::REMOTE;
+ cmd.name = F("X_SendKey");
+ String text = F("NRC_HDMI");
+ text += (input - 1);
+ text += F("-ONOFF");
+
+ setParams(cmd, text);
+
+ return sendRequest(cmd);
+}
+
+bool Client::launch(const String& applicationId)
+{
+ Command cmd;
+ cmd.type = Command::Type::REMOTE;
+ cmd.name = F("X_LaunchApp");
+
+ String text =
+ F("vc_appproduct_id=") + applicationId + F("");
+
+ setParams(cmd, text);
+
+ return sendRequest(cmd);
+}
+
+bool Client::getVolume(GetVolumeCallback onVolume)
+{
+ RequestCompletedDelegate requestCallback = [this, onVolume](HttpConnection& connection, bool successful) -> int {
+ /* @see: docs/RequestResponse.txt for sample communication */
+ CStringArray path("s:Body");
+ path.add("u:GetVolumeResponse");
+ path.add("CurrentVolume");
+
+ auto node = this->getNode(connection, path);
+ if(node != nullptr) {
+ onVolume((int)node->value());
+
+ return true;
+ }
+
+ return false;
+ };
+
+ Command cmd;
+ cmd.type = Command::Type::RENDER;
+ cmd.name = "GetVolume";
+
+ String text = "0Master";
+
+ setParams(cmd, text);
+
+ return sendRequest(cmd, requestCallback);
+}
+
+bool Client::setVolume(size_t volume)
+{
+ if(volume > 100) {
+ debug_e("Volume must be in range from 0 to 100");
+ return false;
+ }
+
+ Command cmd;
+ cmd.type = Command::Type::RENDER;
+ cmd.name = "SetVolume";
+ String text = "0Master";
+ text += volume;
+ text += "";
+
+ setParams(cmd, text);
+
+ return sendRequest(cmd);
+}
+
+bool Client::getMute(GetMuteCallback onMute)
+{
+ RequestCompletedDelegate requestCallback = [this, onMute](HttpConnection& connection, bool successful) -> int {
+ /* @see: docs/RequestResponse.txt for sample communication */
+ CStringArray path("s:Body");
+ path.add("u:GetMuteResponse");
+ path.add("CurrentMute");
+ auto node = this->getNode(connection, path);
+ if(node != nullptr) {
+ onMute((bool)node->value());
+
+ return true;
+ }
+
+ return false;
+ };
+
+ Command cmd;
+ cmd.type = Command::Type::RENDER;
+ cmd.name = "GetMute";
+
+ String text = "0Master";
+
+ setParams(cmd, text);
+
+ return sendRequest(cmd, requestCallback);
+}
+
+bool Client::setMute(bool enable)
+{
+ Command cmd;
+ cmd.type = Command::Type::RENDER;
+ cmd.name = "SetMute";
+
+ String text = "0Master";
+ text += (enable ? '1' : '0');
+ text += "";
+
+ setParams(cmd, text);
+
+ return sendRequest(cmd);
+}
+
+bool Client::sendRequest(Command command, RequestCompletedDelegate requestCallack)
+{
+ String path = F("/nrc/control_0");
+ String urn = F("panasonic-com:service:p00NetworkControl:1");
+ if(command.type == Command::Type::RENDER) {
+ path = F("/dmr/control_0");
+ urn = F("schemas-upnp-org:service:RenderingControl:1");
+ }
+
+ actionTag = nullptr;
+
+ if(!envelope.initialise()) {
+ return false;
+ }
+
+ auto body = envelope.body();
+ if(body == nullptr) {
+ return false;
+ }
+
+ String tag = "u:" + command.name;
+ actionTag = XML::appendNode(body, tag);
+ XML::appendAttribute(actionTag, "xmlns:u", urn);
+
+ if(command.params != nullptr) {
+ auto doc = body->document();
+ auto commandNode = doc->first_node("s:Envelope")->first_node("s:Body")->first_node(tag.c_str());
+ for(XML::Node* child = command.params->first_node(); child; child = child->next_sibling()) {
+ auto node = doc->clone_node(child);
+ commandNode->append_node(node);
+ }
+ }
+
+ const String content = XML::serialize(envelope.doc, false);
+
+ debug_d("Content XML: %s\n", content.c_str());
+
+ HttpHeaders headers;
+ headers[HTTP_HEADER_CONTENT_TYPE] = F("text/xml; charset=\"utf-8\"");
+ headers["SOAPACTION"] = "\"urn:" + urn + '#' + command.name + '"';
+
+ HttpRequest* request = new HttpRequest;
+ request->method = HTTP_POST;
+ request->headers.setMultiple(headers);
+ request->uri = getDescriptionUrl();
+ request->uri.Path = path;
+ request->setBody(content);
+
+ if(requestCallack != nullptr) {
+ request->onRequestComplete(requestCallack);
+ }
+
+ return http.send(request);
+}
+
+} // namespace VieraTV
+
+} // namespace Panasonic
diff --git a/Sming/Libraries/Panasonic-Viera/src/Panasonic/VieraTV/Client.h b/Sming/Libraries/Panasonic-Viera/src/Panasonic/VieraTV/Client.h
new file mode 100644
index 0000000000..e18e1503eb
--- /dev/null
+++ b/Sming/Libraries/Panasonic-Viera/src/Panasonic/VieraTV/Client.h
@@ -0,0 +1,217 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+using namespace rapidxml;
+
+#define VIERA_COMMAND_MAP(XX) \
+ /* action, description */ \
+ XX(CH_DOWN, "channel down") \
+ XX(CH_UP, "channel up") \
+ XX(VOLUP, "volume up") \
+ XX(VOLDOWN, "volume down") \
+ XX(POWER, "power off") \
+ XX(MUTE, "mute") \
+ XX(TV, "TV") \
+ XX(CHG_INPUT, "AV") \
+ XX(HOME, "home screen") \
+ XX(APPS, "apps") \
+ XX(GUIDE, "guide") \
+ XX(RED, "red button") \
+ XX(GREEN, "green") \
+ XX(YELLOW, "yellow") \
+ XX(BLUE, "blue") \
+ XX(VTOOLS, "VIERA tools") \
+ XX(CANCEL, "Cancel / Exit") \
+ XX(SUBMENU, "Option") \
+ XX(RETURN, "Return") \
+ XX(ENTER, "Control Center click / enter") \
+ XX(RIGHT, "Control RIGHT") \
+ XX(LEFT, "Control LEFT") \
+ XX(UP, "Control UP") \
+ XX(DOWN, "Control DOWN") \
+ XX(3D, "3D button") \
+ XX(SD_CARD, "SD-card") \
+ XX(DISP_MODE, "Display mode / Aspect ratio") \
+ XX(MENU, "Menu") \
+ XX(INTERNET, "VIERA connect") \
+ XX(VIERA_LINK, "VIERA link") \
+ XX(EPG, "Guide / EPG") \
+ XX(TEXT, "Text / TTV") \
+ XX(STTL, "STTL / Subtitles") \
+ XX(INFO, "info") \
+ XX(INDEX, "TTV index") \
+ XX(HOLD, "TTV hold / image freeze") \
+ XX(R_TUNE, "Last view") \
+ XX(REW, "rewind") \
+ XX(PLAY, "play") \
+ XX(FF, "fast forward") \
+ XX(SKIP_PREV, "skip previous") \
+ XX(PAUSE, "pause") \
+ XX(SKIP_NEXT, "skip next") \
+ XX(STOP, "stop") \
+ XX(REC, "record")
+
+/* A quick way to check if an app is existing on your TV, if it does not support X_GetAppList is to check
+ * whether the application icon is present.
+ *
+ * For example: http://192.168.2.222:55000/nrc/app_icon/0010000100000001 will get the icon for Amazon Prime Video
+ */
+
+#define VIERA_APP_MAP(XX) \
+ /** id, name, code */ \
+ XX(NETFLIX, "Netflix", 0010000200000001) \
+ XX(YOUTUBE, "YouTube", 0070000200000001) \
+ XX(MEDIA_PLAYER, "Media Player", 0387878700000032) \
+ XX(AMAZON, "Amazon Prime Video", 0010000100000001) \
+ XX(PLEX, "Plex", 0076010507000001) \
+ XX(BBC_IPLAYER, "BBC iPlayer", 0020000A00170010) \
+ XX(BBC_NEWS, "BBC News", 0020000A00170006) \
+ XX(BBC_SPORT, "BBC Sport", 0020000A00170007) \
+ XX(ITV_HUB, "ITV Hub", 0387878700000124) \
+ XX(TUNE_INE, "TuneIn", 0010001800000001) \
+ XX(ACCU_WATHER, "AccuWeather", 0070000C00000001) \
+ XX(ALL_4, "All 4", 0387878700000125) \
+ XX(DEMAND_5, "Demand 5", 0020009300000002) \
+ XX(RAKUTEN, "Rakuten TV", 0020002A00000001) \
+ XX(CHILI, "CHILI", 0020004700000001) \
+ XX(STV_PLAYER, "STV Player", 0387878700000132) \
+ XX(DIGITAL_CONCERT_HALL, "Digital Concert Hall", 0076002307170001) \
+ XX(APPS_MARKET, "Apps Market", 0387878700000102) \
+ XX(BROWSER, "Browser", 0077777700140002) \
+ XX(CALENDAR, "Calendar", 0387878700150020) \
+ XX(VIERA_LINK, "VIERA Link", 0387878700000016) \
+ XX(RECORDED_TV, "Recorded TV", 0387878700000013) \
+ XX(FREEVIEW_CATCH_UP, "Freeview Catch Up", 0387878700000109)
+
+namespace Panasonic
+{
+namespace VieraTV
+{
+enum class Action {
+#define XX(name, description) ACTION_##name,
+ VIERA_COMMAND_MAP(XX)
+#undef XX
+};
+
+enum class ApplicationId {
+#define XX(id, name, code) APP_##id,
+ VIERA_APP_MAP(XX)
+#undef XX
+};
+
+String toString(enum Action a);
+String toString(enum ApplicationId a);
+
+class Client : public Dial::Client
+{
+public:
+ using GetMuteCallback = Delegate;
+ using GetVolumeCallback = Delegate;
+
+ struct Command {
+ enum class Type {
+ REMOTE,
+ RENDER,
+ };
+
+ Type type;
+ String name; // How device identifies itself
+ XML::Document* params;
+ };
+
+ Client(size_t maxDescriptionSize = 4096) : Dial::Client(maxDescriptionSize)
+ {
+ }
+
+ using Dial::Client::connect;
+
+ bool connect(ConnectedCallback callback,
+ const String& type = "urn:panasonic-com:device:p00RemoteController:1") override
+ {
+ return Dial::Client::connect(callback, type);
+ }
+
+ /**
+ * Send a command to the TV
+ *
+ * @param action command Command from codes.txt
+ */
+ bool sendCommand(Action action);
+
+ /**
+ * Send a change HDMI input to the TV
+ *
+ * @param input
+ */
+ bool switchToHdmi(size_t input);
+
+ /**
+ * Send command to open app on the TV
+ *
+ * @param id
+ */
+ bool launch(enum ApplicationId id)
+ {
+ return launch(toString(id));
+ }
+
+ /**
+ * Send command to open app on the TV
+ *
+ * @param {String} applicationId appId from codes.txt
+ */
+ bool launch(const String& applicationId);
+
+ /**
+ * Get volume from TV
+ *
+ * @param callback
+ * @return bool - true on success false otherwise
+ */
+ bool getVolume(GetVolumeCallback onVolume);
+
+ /**
+ * Set volume
+ *
+ * @param {Int} volume Desired volume in range from 0 to 100
+ */
+ bool setVolume(size_t volume);
+
+ /**
+ * Get the current mute setting
+ *
+ * @param callback
+ * @return bool - true on success false otherwise
+ */
+ bool getMute(GetMuteCallback onMute);
+
+ /**
+ * Set mute to on/off
+ *
+ * @param {Boolean} enable The value to set mute to
+ */
+ bool setMute(bool enable);
+
+private:
+ SOAP::Envelope envelope;
+ XML::Node* actionTag = nullptr;
+ XML::Document paramsDoc;
+
+ bool sendRequest(Command command, RequestCompletedDelegate requestCallack = nullptr);
+
+ bool setParams(Command& cmd, const String& text)
+ {
+ cmd.params = ¶msDoc;
+ cmd.params->parse<0>((char*)text.c_str());
+ return true;
+ }
+};
+
+} // namespace VieraTV
+
+} // namespace Panasonic