Skip to content

Instantly share code, notes, and snippets.

@renssies
Created February 8, 2024 23:34
Show Gist options
  • Save renssies/691f9acc6eb91fd90ffa62da3777ef63 to your computer and use it in GitHub Desktop.
Save renssies/691f9acc6eb91fd90ffa62da3777ef63 to your computer and use it in GitHub Desktop.
Control LG WebOS TV using ESP32 and Arduino (PlatformIO)
; 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.
/*
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