Created
February 8, 2024 23:34
-
-
Save renssies/691f9acc6eb91fd90ffa62da3777ef63 to your computer and use it in GitHub Desktop.
Control LG WebOS TV using ESP32 and Arduino (PlatformIO)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; PlatformIO Project Configuration File | |
; | |
; Build options: build flags, source filter | |
; Upload options: custom upload port, speed and extra flags | |
; Library options: dependencies, extra library storages | |
; Advanced options: extra scripting | |
; | |
; Please visit documentation for the other options and examples | |
; https://docs.platformio.org/page/projectconf.html | |
[env:esp32doit-devkit-v1] | |
platform = espressif32 | |
board = esp32doit-devkit-v1 ; Change to your own board | |
framework = arduino | |
lib_deps = | |
links2004/WebSockets@^2.4.1 | |
bblanchon/ArduinoJson@^6.21.5 | |
monitor_speed = 115200 | |
build_flags = "-DDEBUG_ESP_PORT=Serial" ; Enables debugging the websocket connection over serial. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Control a LG WebOS TV using ESP32. This file is made using PlatformIO but Arduino should be supported as well. | |
*/ | |
// Arduino libraries | |
#include <Arduino.h> | |
// ESP32 Framework libraries | |
#include <WiFi.h> | |
#include <WiFiClientSecure.h> | |
#include <Preferences.h> | |
// External libraries | |
#include <WebSocketsClient.h> // Uses links2004/WebSockets@^2.4.1 | |
#include <ArduinoJson.h> // Uses bblanchon/ArduinoJson@^6.21.5 | |
#define PREF_NAMESPACE "esp32lg" | |
#define PREF_KEY_CLIENT_KEY "client-key" | |
// Enter your details here. | |
const char *wifiSSID = "<<Your SSID>>"; | |
const char *wifiPassword = "<<Your Password>>"; | |
const char *tvIPAddress = "<<TV IP Address (example: 10.0.1.8)>>"; | |
WebSocketsClient webSocket; | |
Preferences preferences; | |
// If the websocket session is paired to the TV, sending commands only works if it's true. | |
bool isPaired = false; | |
// The client key for this ESP, it's generated by the TV the first time this program is ran. | |
String clientKey = ""; | |
// If the toast has been displayed, see `loop()` | |
bool toastDisplayed = false; | |
void setup() { | |
Serial.begin(115200); | |
Serial.setDebugOutput(true); | |
Serial.println("Connecting to WiFi"); | |
WiFi.begin(wifiSSID, wifiPassword); | |
while (WiFi.status() != WL_CONNECTED) { | |
Serial.print('.'); | |
delay(50); | |
} | |
Serial.println("Connected!"); | |
// Register the event handler | |
webSocket.onEvent(webSocketEvent); | |
// Try every 5000 again if connection has failed | |
webSocket.setReconnectInterval(5000); | |
webSocket.begin(tvIPAddress, 3000, "/", ""); // The last argument is important, if (Sec-Websocket-)Protocol is filled the LG TV will reject it. | |
Serial.println("Entering loop"); | |
} | |
void loop() { | |
webSocket.loop(); | |
if (webSocket.isConnected() && isPaired && !toastDisplayed) { | |
// Display the toast if the websocket is connected, paired and a toast hasn't been displayed yet. | |
displayToast("Hello world, this is an ESP32"); | |
toastDisplayed = true; | |
} | |
} | |
void webSocketEvent(WStype_t type, uint8_t *payload, size_t length) { | |
switch (type) { | |
case WStype_DISCONNECTED: | |
Serial.println("Websocket Disconnected!"); | |
isPaired = false; | |
break; | |
case WStype_CONNECTED: { | |
Serial.printf("Connected to websocket at URL: %s\n", payload); | |
// We are connected to the LG websocket | |
// Next we need to check if we have a client key. | |
// If we have a client key we need to register this session with a client key | |
// If we don't have a client key we need to prompt the user to get one. | |
preferences.begin(PREF_NAMESPACE, true); | |
if (preferences.isKey(PREF_KEY_CLIENT_KEY)) { | |
// We have a client key stored, use it to register with a key. | |
clientKey = preferences.getString(PREF_KEY_CLIENT_KEY); | |
Serial.print("Registering with client key: "); | |
Serial.println(clientKey); | |
registerWithClientKey(clientKey); | |
} else { | |
// Prompt the TV for a client key | |
promptForRegistration(); | |
Serial.println("Accept the pairing on the TV!"); | |
} | |
preferences.end(); | |
} | |
break; | |
case WStype_TEXT: { | |
Serial.printf("Got text from websocket: %s\n", payload); | |
StaticJsonDocument<1024> doc; | |
DeserializationError error = deserializeJson(doc, (char*)payload, length); | |
if (error) { | |
Serial.print("deserializeJson() failed: "); | |
Serial.println(error.c_str()); | |
} else if (doc.containsKey("type")) { | |
if (doc["type"].as<String>() == String("registered")) { | |
// We are registered, we have a client key we need to store, even if we already have one. | |
clientKey = doc["payload"]["client-key"].as<String>(); | |
Serial.println("Registered!"); | |
Serial.print("Client key: "); | |
Serial.println(clientKey); | |
// Store the client key | |
preferences.begin(PREF_NAMESPACE, false); | |
preferences.putString(PREF_KEY_CLIENT_KEY, clientKey); | |
preferences.end(); | |
isPaired = true; | |
} | |
} | |
} | |
break; | |
case WStype_ERROR: | |
Serial.println("An error occurred with the websocket"); | |
break; | |
case WStype_BIN: | |
case WStype_FRAGMENT_TEXT_START: | |
case WStype_FRAGMENT_BIN_START: | |
case WStype_FRAGMENT: | |
case WStype_FRAGMENT_FIN: | |
break; | |
} | |
} | |
// Display a toast on the LG TV. | |
void displayToast(String message) { | |
if(!isPaired) { | |
// We are not paired, we can't send messages. | |
return; | |
} | |
if (!webSocket.isConnected()) { | |
// Websocket is not connected, we can't send messages. | |
return; | |
} | |
StaticJsonDocument<1024> doc; | |
doc["id"] = "message"; | |
doc["type"] = "request"; | |
doc["uri"] = "ssap://system.notifications/createToast"; | |
doc["payload"]["message"] = message; | |
String toastCommand = ""; | |
serializeJson(doc, toastCommand); | |
webSocket.sendTXT(toastCommand); | |
} | |
// Registers the websocket session with the existing client key. This will trigger a new client key we need to store as well. | |
void registerWithClientKey(String clientKey) { | |
sendRegisterCommand(clientKey); | |
} | |
// Triggers the prompt on the TV to pair the LG Remote App. | |
void promptForRegistration() { | |
sendRegisterCommand(""); | |
} | |
// Send the register command. Use an empty string to trigger the pairing prompt. | |
void sendRegisterCommand(String clientKey) { | |
/* | |
DO NOT CHANGE THE JSON HERE! | |
*/ | |
DynamicJsonDocument doc(3072); | |
doc["type"] = "register"; | |
doc["id"] = "register_0"; | |
JsonObject payload = doc.createNestedObject("payload"); | |
payload["forcePairing"] = false; | |
payload["pairingType"] = "PROMPT"; | |
if (clientKey.length() > 0) { | |
payload["client-key"] = clientKey; | |
} | |
JsonObject payload_manifest = payload.createNestedObject("manifest"); | |
payload_manifest["manifestVersion"] = 1; | |
payload_manifest["appVersion"] = "1.1"; | |
JsonObject payload_manifest_signed = payload_manifest.createNestedObject("signed"); | |
payload_manifest_signed["created"] = "20140509"; | |
payload_manifest_signed["appId"] = "com.lge.test"; | |
payload_manifest_signed["vendorId"] = "com.lge"; | |
JsonObject payload_manifest_signed_localizedAppNames = payload_manifest_signed.createNestedObject("localizedAppNames"); | |
payload_manifest_signed_localizedAppNames[""] = "LG Remote App"; | |
payload_manifest_signed_localizedAppNames["ko-KR"] = "리모컨 앱"; | |
payload_manifest_signed_localizedAppNames["zxx-XX"] = "ЛГ Rэмotэ AПП"; | |
payload_manifest_signed["localizedVendorNames"][""] = "LG Electronics"; | |
JsonArray payload_manifest_signed_permissions = payload_manifest_signed.createNestedArray("permissions"); | |
payload_manifest_signed_permissions.add("TEST_SECURE"); | |
payload_manifest_signed_permissions.add("CONTROL_INPUT_TEXT"); | |
payload_manifest_signed_permissions.add("CONTROL_MOUSE_AND_KEYBOARD"); | |
payload_manifest_signed_permissions.add("READ_INSTALLED_APPS"); | |
payload_manifest_signed_permissions.add("READ_LGE_SDX"); | |
payload_manifest_signed_permissions.add("READ_NOTIFICATIONS"); | |
payload_manifest_signed_permissions.add("SEARCH"); | |
payload_manifest_signed_permissions.add("WRITE_SETTINGS"); | |
payload_manifest_signed_permissions.add("WRITE_NOTIFICATION_ALERT"); | |
payload_manifest_signed_permissions.add("CONTROL_POWER"); | |
payload_manifest_signed_permissions.add("READ_CURRENT_CHANNEL"); | |
payload_manifest_signed_permissions.add("READ_RUNNING_APPS"); | |
payload_manifest_signed_permissions.add("READ_UPDATE_INFO"); | |
payload_manifest_signed_permissions.add("UPDATE_FROM_REMOTE_APP"); | |
payload_manifest_signed_permissions.add("READ_LGE_TV_INPUT_EVENTS"); | |
payload_manifest_signed_permissions.add("READ_TV_CURRENT_TIME"); | |
payload_manifest_signed["serial"] = "2f930e2d2cfe083771f68e4fe7bb07"; | |
JsonArray payload_manifest_permissions = payload_manifest.createNestedArray("permissions"); | |
payload_manifest_permissions.add("LAUNCH"); | |
payload_manifest_permissions.add("LAUNCH_WEBAPP"); | |
payload_manifest_permissions.add("APP_TO_APP"); | |
payload_manifest_permissions.add("CLOSE"); | |
payload_manifest_permissions.add("TEST_OPEN"); | |
payload_manifest_permissions.add("TEST_PROTECTED"); | |
payload_manifest_permissions.add("CONTROL_AUDIO"); | |
payload_manifest_permissions.add("CONTROL_DISPLAY"); | |
payload_manifest_permissions.add("CONTROL_INPUT_JOYSTICK"); | |
payload_manifest_permissions.add("CONTROL_INPUT_MEDIA_RECORDING"); | |
payload_manifest_permissions.add("CONTROL_INPUT_MEDIA_PLAYBACK"); | |
payload_manifest_permissions.add("CONTROL_INPUT_TV"); | |
payload_manifest_permissions.add("CONTROL_POWER"); | |
payload_manifest_permissions.add("READ_APP_STATUS"); | |
payload_manifest_permissions.add("READ_CURRENT_CHANNEL"); | |
payload_manifest_permissions.add("READ_INPUT_DEVICE_LIST"); | |
payload_manifest_permissions.add("READ_NETWORK_STATE"); | |
payload_manifest_permissions.add("READ_RUNNING_APPS"); | |
payload_manifest_permissions.add("READ_TV_CHANNEL_LIST"); | |
payload_manifest_permissions.add("WRITE_NOTIFICATION_TOAST"); | |
payload_manifest_permissions.add("READ_POWER_STATE"); | |
payload_manifest_permissions.add("READ_COUNTRY_INFO"); | |
JsonObject payload_manifest_signatures_0 = payload_manifest["signatures"].createNestedObject(); | |
payload_manifest_signatures_0["signatureVersion"] = 1; | |
payload_manifest_signatures_0["signature"] = "eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw=="; | |
String registerCommand = ""; | |
serializeJson(doc, registerCommand); | |
webSocket.sendTXT(registerCommand); | |
/* | |
DO NOT CHANGE THE JSON HERE! | |
*/ | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment