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