├── .gitignore ├── ClientApp.png ├── LICENSE ├── README.md ├── RemoteDisplay.cpp └── RemoteDisplay.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | -------------------------------------------------------------------------------- /ClientApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeCoders/LVGLRemoteServer/9407da62c54842e009a82b68091a7aa8c04b147b/ClientApp.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CubeCoders Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remote Display for LVGL Development 2 | 3 | This project provides a remote display tool to aid in the development of LVGL (Light and Versatile Graphics Library) applications. It allows you to control and test LVGL-based apps from your desktop, even if the hardware does not have a physical display attached during early development. 4 | 5 | It's also great for taking screenshots or capturing videos for demonstration purposes without having to rely on separate storage or IO. This library can be used on a bare ESP32 development board with nothing attached other than power. 6 | 7 | ![Screenshot of the client interface](https://github.com/CubeCoders/LVGLRemoteServer/blob/main/ClientApp.png?raw=true) 8 | 9 | ## Features 10 | 11 | - **Remote Display Control**: Control LVGL applications from your desktop. 12 | - **RLE Packet Transmission**: Efficiently transmit Run-Length Encoded (RLE) packets. 13 | - **UDP Communication**: Information is kept within single UDP packets for simplicity. 14 | - **Touch Input Simulation**: Simulate touch input on the remote display. 15 | - **Zero Allocation**: No dynamic allocation is performed, so the memory footprint is constant. 16 | 17 | ## Limitations 18 | 19 | - **16-bit RGB565 Color Only**: No other color depths are supported. 20 | - **No security or encryption**: This is absolutely not safe for use over public networks or in production. This is development tool only. 21 | 22 | ## Files 23 | 24 | - `RemoteDisplay.cpp`: Implementation of the remote display functionalities. 25 | - `RemoteDisplay.h`: Header file for the remote display class. 26 | 27 | ## Client application 28 | 29 | Only available for Windows at this time, but it may run on Linux/MacOS via Mono. Download the client signed with our digital certificate here: [Client Application Link](https://downloads.cubecoders.com/LVGLRemote/Viewer_Signed.zip) 30 | 31 | ## Usage 32 | 33 | ### Arduino IDE users 34 | 35 | Place `RemoteDisplay.cpp` and `RemoteDisplay.h` in a directory in your Arduino Libraries directory `Arduino/libraries/LVGLRemoteServer` and they will be available in any Arduino projects. 36 | 37 | ### Setup Example 38 | 39 | ```cpp 40 | 41 | #include 42 | #include 43 | 44 | /* If you are configuring networking yourself, you can omit these parameters */ 45 | RemoteDisplay remoteDisplay("Wifi AP Name", "Wifi Password"); 46 | 47 | void disp_flush(lv_display_t* disp, const lv_area_t* area, uint8_t* pixelmap) { 48 | remoteDisplay.sendRLE(area, pixelmap); 49 | 50 | /* Your existing disp_flush code */ 51 | 52 | lv_disp_flush_ready(disp); 53 | } 54 | 55 | void setup() 56 | { 57 | lv_init(); 58 | lv_display_t* disp = lv_display_create(SCREEN_WIDTH, SCREEN_HEIGHT); 59 | lv_display_set_buffers(disp, buf, NULL, SCREENBUFFER_SIZE_PIXELS * sizeof(lv_color_t), LV_DISPLAY_RENDER_MODE_PARTIAL); 60 | 61 | /*If you have an existing disp_flush method, this doesn't change*/ 62 | lv_display_set_flush_cb(disp, disp_flush); 63 | 64 | /*If you don't have a display and want to use the remote display only, you can use its internal handler*/ 65 | //lv_display_set_flush_cb(disp, remoteDisplay.remoteDispFlush); 66 | 67 | remoteDisplay.init(disp); 68 | 69 | /*Optional: Initiate the connection from the server side so the display is sent to a client immediately. 70 | Useful with the clients kiosk mode for demonstrations or presentations. */ 71 | remoteDisplay.connectRemote("192.168.100.50"); 72 | } 73 | 74 | ``` 75 | 76 | ## Functions 77 | 78 | - [`RemoteDisplay::init`](RemoteDisplay.cpp): Initialize, configure LVGL and optionally connect to WiFi. 79 | - [`RemoteDisplay::send`](RemoteDisplay.cpp): Sends display data uncompressed (sometimes better for noisy data such as photos) 80 | - [`RemoteDisplay::sendRLE`](RemoteDisplay.cpp): Sends display data RLE encoded. 81 | - [`RemoteDisplay::remoteDispFlush`](RemoteDisplay.cpp): Flushes the display. 82 | 83 | ## Dependencies 84 | 85 | - LVGL 9.x 86 | - WiFi 87 | - WiFiUdp 88 | 89 | ## License 90 | 91 | This project is licensed under the MIT License. 92 | ©2024 CubeCoders Limited 93 | -------------------------------------------------------------------------------- /RemoteDisplay.cpp: -------------------------------------------------------------------------------- 1 | #include "RemoteDisplay.h" 2 | 3 | RemoteDisplay::RemoteDisplay(int udpPort, const char* ssid, const char* password) 4 | : udpPort(udpPort), ssid(ssid), password(password), udpAddress(0), wifiConnected(false) {} 5 | 6 | RemoteDisplay::RemoteDisplay(const char* ssid, const char* password) 7 | : udpPort(24680), ssid(ssid), password(password), udpAddress(0), wifiConnected(false) {} 8 | 9 | void RemoteDisplay::init(lv_display_t *disp) { 10 | if (ssid != nullptr && password != nullptr) { 11 | WiFi.begin(ssid, password); 12 | while (WiFi.status() != WL_CONNECTED) { 13 | delay(100); 14 | Serial.print("."); 15 | } 16 | Serial.println(""); 17 | Serial.println("WiFi connected."); 18 | Serial.print("IP Address:"); 19 | Serial.println(WiFi.localIP()); 20 | wifiConnected = true; 21 | } 22 | 23 | udp.begin(udpPort); 24 | 25 | static lv_indev_t *remotedev = lv_indev_create(); 26 | lv_indev_set_type(remotedev, LV_INDEV_TYPE_POINTER); 27 | lv_indev_set_read_cb(remotedev, [](lv_indev_t* indev, lv_indev_data_t* data){ 28 | RemoteDisplay* instance = static_cast(lv_indev_get_user_data(indev)); 29 | instance->readRemoteTouch(indev, data); 30 | }); 31 | 32 | lv_indev_set_user_data(remotedev, this); 33 | } 34 | 35 | void RemoteDisplay::connectRemote(char *ipStr) { 36 | IPAddress ip; 37 | if (ip.fromString(ipStr)) { 38 | connectRemote(ip); 39 | } 40 | } 41 | 42 | void RemoteDisplay::connectRemote(IPAddress ip) { 43 | udpAddress = ip; 44 | transmitInfoPacket(); 45 | refreshDisplay(); 46 | } 47 | 48 | void RemoteDisplay::send(const lv_area_t *area, uint8_t *pixelmap) { 49 | if (udpAddress == 0) 50 | { 51 | return; 52 | } 53 | uint32_t fullWidth = (area->x2 - area->x1 + 1); // Full width of the display area 54 | uint32_t fullHeight = (area->y2 - area->y1 + 1); 55 | size_t bytesPerPixel = 2; // 16bpp (RGB565 format) 56 | 57 | // Serial.printf("Transmitting area X:%d, Y:%d %dx%d\n", area->x1, area->y1, fullWidth, fullHeight); 58 | 59 | // Tile size: 32x16 pixels (each tile is 1024 bytes of pixel data) 60 | const uint32_t tileWidth = 40; 61 | const uint32_t tileHeight = 16; 62 | 63 | // Calculate the number of tiles in X and Y directions 64 | uint32_t numTilesX = (fullWidth + tileWidth - 1) / tileWidth; // Ceiling division 65 | uint32_t numTilesY = (fullHeight + tileHeight - 1) / tileHeight; 66 | 67 | // Loop through the number of tiles 68 | for (uint32_t tileIndexY = 0; tileIndexY < numTilesY; tileIndexY++) 69 | { 70 | for (uint32_t tileIndexX = 0; tileIndexX < numTilesX; tileIndexX++) 71 | { 72 | // Calculate the top-left corner of the tile 73 | uint32_t tileX = tileIndexX * tileWidth; 74 | uint32_t tileY = tileIndexY * tileHeight; 75 | 76 | // Calculate the actual tile width and height safely 77 | uint32_t actualTileWidth = min(tileWidth, fullWidth - tileX); 78 | uint32_t actualTileHeight = min(tileHeight, fullHeight - tileY); 79 | 80 | // Calculate the size of the tile's pixel data 81 | size_t actualTileDataSize = actualTileWidth * actualTileHeight * bytesPerPixel; 82 | 83 | uint16_t controlValue = 0x00; 84 | uint16_t sendX = (area->x1 + tileX); 85 | uint16_t sendY = (area->y1 + tileY); 86 | uint16_t sendW = actualTileWidth; 87 | uint16_t sendH = actualTileHeight; 88 | 89 | // Create buffer to hold position, size, and raw pixel data 90 | size_t packetSize = sizeof(controlValue) + sizeof(sendX) + sizeof(sendY) + sizeof(sendW) + sizeof(sendH); 91 | size_t totalPacketSize = packetSize + actualTileDataSize; 92 | 93 | memcpy(packetBuffer, &controlValue, sizeof(controlValue)); // transmitted X position 94 | memcpy(packetBuffer + sizeof(controlValue), &sendX, sizeof(sendX)); // transmitted X position 95 | memcpy(packetBuffer + sizeof(controlValue) + sizeof(sendX), &sendY, sizeof(sendY)); // transmitted Y position 96 | memcpy(packetBuffer + sizeof(controlValue) + sizeof(sendX) + sizeof(sendY), &sendW, sizeof(sendW)); // width 97 | memcpy(packetBuffer + sizeof(controlValue) + sizeof(sendX) + sizeof(sendY) + sizeof(sendW), &sendH, sizeof(sendH)); // height 98 | 99 | // Now we need to extract the pixel data for this tile from the full pixelmap 100 | uint8_t *tileDataPtr = packetBuffer + packetSize; 101 | 102 | for (uint32_t row = 0; row < actualTileHeight; row++) 103 | { 104 | // Calculate the start of the current row in the full image 105 | size_t srcOffset = (tileY + row) * fullWidth * bytesPerPixel + (tileX * bytesPerPixel); 106 | size_t destOffset = row * actualTileWidth * bytesPerPixel; 107 | 108 | // Copy the correct number of bytes for each row 109 | memcpy(tileDataPtr + destOffset, pixelmap + srcOffset, actualTileWidth * bytesPerPixel); 110 | } 111 | 112 | // Send the tile via UDP 113 | udp.beginPacket(udpAddress, udpPort); 114 | udp.write(packetBuffer, totalPacketSize); 115 | udp.endPacket(); 116 | } 117 | } 118 | } 119 | 120 | int getPaletteIndex(uint16_t* palette, int paletteSize, uint16_t color) { 121 | for (int i = 0; i < paletteSize; i++) { 122 | if (palette[i] == color) { 123 | return i; // Return the index if color is found 124 | } 125 | } 126 | return 0; 127 | } 128 | 129 | void RemoteDisplay::sendPalettedRLE(const lv_area_t *area, uint8_t *pixelmap) { 130 | const int maxPaletteSize = 16; // Hardcoded limit 131 | int paletteSize = 0; 132 | uint32_t fullWidth = area->x2 - area->x1 + 1; 133 | uint32_t fullHeight = area->y2 - area->y1 + 1; 134 | uint32_t totalPixels = fullWidth * fullHeight; 135 | size_t bytesPerPixel = 2; // 16bpp (RGB565 format) 136 | 137 | // Traverse the pixel data and build the palette using paletteBuffer 138 | for (uint32_t pixel = 0; pixel < totalPixels; pixel++) { 139 | uint16_t color = pixelmap[pixel * bytesPerPixel] | (pixelmap[pixel * bytesPerPixel + 1] << 8); 140 | 141 | // Check if color is already in the palette 142 | bool found = false; 143 | for (int i = 0; i < paletteSize; i++) { 144 | if (paletteBuffer[i] == color) { 145 | found = true; 146 | break; 147 | } 148 | } 149 | 150 | // Add the color to the palette if not found 151 | if (!found) { 152 | if (paletteSize < maxPaletteSize) { 153 | paletteBuffer[paletteSize++] = color; 154 | } else { 155 | // Exceeded palette size, fall back to standard RLE 156 | sendRLE(area, pixelmap); 157 | return; 158 | } 159 | } 160 | } 161 | 162 | uint32_t rleCount = 0; // Number of RLE entries in the buffer 163 | uint32_t rleLength = 0; // Index for the rleBuffer 164 | uint16_t runLength = 1; // Current run length 165 | uint32_t totalPixelsProcessed = 0; 166 | uint32_t runDataPosition = 0; 167 | uint32_t pixelsInPacket = 0; 168 | 169 | memcpy(rleBuffer, paletteBuffer, paletteSize * 2); 170 | rleLength = paletteSize; 171 | const size_t maxRLEEntries = (MAX_PACKET_SIZE - 10 - (paletteSize * sizeof(uint16_t))) / 2; // Each RLE entry is 2 bytes - 10 byte header - 2 bytes per palette entry 172 | 173 | // Initialize lastColor with the first pixel color 174 | uint16_t lastColor = pixelmap[0] | (pixelmap[1] << 8); 175 | 176 | // Traverse through the pixel data 177 | for (uint32_t pixel = 1; pixel <= totalPixels; pixel++) 178 | { 179 | uint16_t color = 0; 180 | uint8_t colorPaletteIndex = 0; 181 | if (pixel < totalPixels) 182 | { 183 | uint32_t pixelIndex = pixel * bytesPerPixel; 184 | color = pixelmap[pixelIndex] | (pixelmap[pixelIndex + 1] << 8); 185 | colorPaletteIndex = getPaletteIndex(paletteBuffer, paletteSize, color); 186 | } 187 | 188 | bool colorChanged = (pixel == (totalPixels - 1)) || (color != lastColor); 189 | bool runLengthMaxed = (runLength == 255); 190 | bool bufferFull = (rleCount == maxRLEEntries); 191 | 192 | if (colorChanged || runLengthMaxed || bufferFull) 193 | { 194 | // Add the current run to the RLE buffer 195 | rleBuffer[rleLength++] = (colorPaletteIndex << 8) | runLength; 196 | rleCount++; 197 | totalPixelsProcessed += runLength; 198 | pixelsInPacket += runLength; 199 | 200 | // Send the RLE buffer if the buffer is full or if we've processed all pixels 201 | if (bufferFull || pixel == totalPixels) 202 | { 203 | sendRLEPacket(area->x1, area->y1, fullWidth, runDataPosition, rleLength, paletteSize); 204 | runDataPosition = totalPixelsProcessed; 205 | rleLength = paletteSize; 206 | rleCount = 0; 207 | pixelsInPacket = 0; 208 | } 209 | 210 | // Reset runLength and update lastColor 211 | runLength = 1; 212 | lastColor = color; 213 | } 214 | else 215 | { 216 | // Continue the current run 217 | runLength++; 218 | } 219 | } 220 | 221 | // Check if the total number of pixels sent matches the expected totalPixels 222 | if (totalPixelsProcessed != totalPixels) 223 | { 224 | Serial.printf("Discrepancy: Expected %d pixels, but sent %d pixels\n", totalPixels, totalPixelsProcessed); 225 | } 226 | else 227 | { 228 | // Serial.println("All pixels sent successfully"); 229 | } 230 | } 231 | 232 | 233 | void RemoteDisplay::sendRLE(const lv_area_t *area, uint8_t *pixelmap) { 234 | uint32_t fullWidth = (area->x2 - area->x1 + 1); 235 | uint32_t fullHeight = (area->y2 - area->y1 + 1); 236 | uint32_t totalPixels = fullWidth * fullHeight; 237 | size_t bytesPerPixel = 2; // 16bpp (RGB565 format) 238 | 239 | const size_t maxRLEEntries = (MAX_PACKET_SIZE - 10) / 4; // Each RLE entry is 4 bytes - 10 byte header 240 | 241 | // Serial.printf("Transmitting area X:%d, Y:%d %dx%d - %d pixels\n", area->x1, area->y1, fullWidth, fullHeight, totalPixels); 242 | 243 | uint32_t rleCount = 0; // Number of RLE entries in the buffer 244 | uint32_t rleLength = 0; // Index for the rleBuffer 245 | uint16_t runLength = 1; // Current run length 246 | uint32_t totalPixelsProcessed = 0; 247 | uint32_t runDataPosition = 0; 248 | uint32_t pixelsInPacket = 0; 249 | 250 | // Initialize lastColor with the first pixel color 251 | uint16_t lastColor = pixelmap[0] | (pixelmap[1] << 8); 252 | 253 | // Traverse through the pixel data 254 | for (uint32_t pixel = 1; pixel <= totalPixels; pixel++) 255 | { 256 | uint16_t color = 0; 257 | if (pixel < totalPixels) 258 | { 259 | uint32_t pixelIndex = pixel * bytesPerPixel; 260 | color = pixelmap[pixelIndex] | (pixelmap[pixelIndex + 1] << 8); 261 | } 262 | 263 | bool colorChanged = (pixel == (totalPixels - 1)) || (color != lastColor); 264 | bool runLengthMaxed = (runLength == 65535); 265 | bool bufferFull = (rleCount == maxRLEEntries); 266 | 267 | if (colorChanged || runLengthMaxed || bufferFull) 268 | { 269 | // Add the current run to the RLE buffer 270 | rleBuffer[rleLength++] = lastColor; 271 | rleBuffer[rleLength++] = runLength; 272 | rleCount++; 273 | totalPixelsProcessed += runLength; 274 | pixelsInPacket += runLength; 275 | 276 | // Send the RLE buffer if the buffer is full or if we've processed all pixels 277 | if (bufferFull || pixel == totalPixels) 278 | { 279 | sendRLEPacket(area->x1, area->y1, fullWidth, runDataPosition, rleLength); 280 | runDataPosition = totalPixelsProcessed; 281 | rleLength = 0; 282 | rleCount = 0; 283 | pixelsInPacket = 0; 284 | } 285 | 286 | // Reset runLength and update lastColor 287 | runLength = 1; 288 | lastColor = color; 289 | } 290 | else 291 | { 292 | // Continue the current run 293 | runLength++; 294 | } 295 | } 296 | 297 | // Check if the total number of pixels sent matches the expected totalPixels 298 | if (totalPixelsProcessed != totalPixels) 299 | { 300 | Serial.printf("Discrepancy: Expected %d pixels, but sent %d pixels\n", totalPixels, totalPixelsProcessed); 301 | } 302 | else 303 | { 304 | // Serial.println("All pixels sent successfully"); 305 | } 306 | } 307 | 308 | void RemoteDisplay::sendRLEPacket(uint16_t transmittedX, uint16_t transmittedY, uint16_t tileWidth, uint16_t progressStart, uint32_t rleLength, uint8_t paletteSize) { 309 | if (udpAddress == 0) { return; } 310 | 311 | static bool firstPacket = true; 312 | 313 | const uint16_t controlValue = paletteSize == 0 ? 0x0001 : 0x0100 | paletteSize; 314 | 315 | if (firstPacket && paletteSize > 0) { 316 | //Log all the params to serial for debugging 317 | Serial.printf("Transmitting RLE packet: X:%d, Y:%d, Width:%d, Progress:%d, Length:%d, PaletteSize:%d\n", transmittedX, transmittedY, tileWidth, progressStart, rleLength, paletteSize); 318 | //Also log the palette data with indicies as hex 319 | Serial.print("Palette: "); 320 | for (int i = 0; i < paletteSize; i++) { 321 | Serial.printf("%04X ", paletteBuffer[i]); 322 | } 323 | Serial.println(); 324 | firstPacket = false; 325 | } 326 | 327 | // Create buffer to hold position, size, and raw pixel data 328 | size_t packetSize = sizeof(controlValue) + sizeof(transmittedX) + sizeof(transmittedY) + sizeof(tileWidth) + sizeof(progressStart); 329 | size_t totalPacketSize = packetSize + rleLength * sizeof(uint16_t); 330 | 331 | memcpy(packetBuffer, &controlValue, sizeof(controlValue)); // Control value 332 | memcpy(packetBuffer + sizeof(controlValue), &transmittedX, sizeof(transmittedX)); // X 333 | memcpy(packetBuffer + sizeof(controlValue) + sizeof(transmittedX), &transmittedY, sizeof(transmittedY)); // Y 334 | memcpy(packetBuffer + sizeof(controlValue) + sizeof(transmittedX) + sizeof(transmittedY), &tileWidth, sizeof(tileWidth)); // Width 335 | memcpy(packetBuffer + sizeof(controlValue) + sizeof(transmittedX) + sizeof(transmittedY) + sizeof(tileWidth), &progressStart, sizeof(progressStart)); // Progress (start of packet) 336 | 337 | // Copy RLE-encoded pixel data into the packet buffer 338 | memcpy(packetBuffer + packetSize, rleBuffer, rleLength * sizeof(uint16_t)); 339 | 340 | // Send the packet via UDP 341 | udp.beginPacket(udpAddress, udpPort); 342 | udp.write(packetBuffer, totalPacketSize); 343 | udp.endPacket(); 344 | } 345 | 346 | void RemoteDisplay::transmitInfoPacket() { 347 | // Following the same packet format as before, use a control value of 0x0002 to indicate an info packet 348 | // Containing the screen dimensions with the remaining two 16-bit values being empty. 349 | const uint16_t controlValue = 0xFFFF; 350 | uint16_t screenWidth = (uint16_t)LV_HOR_RES; 351 | uint16_t screenHeight = (uint16_t)LV_VER_RES; 352 | 353 | size_t packetSize = sizeof(controlValue) + sizeof(screenWidth) + sizeof(screenHeight) + sizeof(uint16_t) + sizeof(uint16_t); 354 | 355 | memcpy(infoBuffer, &controlValue, sizeof(controlValue)); // Control value 356 | memcpy(infoBuffer + sizeof(controlValue), &screenWidth, sizeof(screenWidth)); // Screen width 357 | memcpy(infoBuffer + sizeof(controlValue) + sizeof(screenWidth), &screenHeight, sizeof(screenHeight)); // Screen height 358 | memset(infoBuffer + sizeof(controlValue) + sizeof(screenWidth) + sizeof(screenHeight), 0, sizeof(uint16_t) * 2); // Remaining two 16-bit values 359 | 360 | // Send the packet via UDP 361 | udp.beginPacket(udpAddress, udpPort); 362 | udp.write(infoBuffer, packetSize); 363 | udp.endPacket(); 364 | } 365 | 366 | void RemoteDisplay::refreshDisplay() { 367 | lv_area_t area; 368 | area.x1 = 0; 369 | area.y1 = 0; 370 | area.x2 = LV_HOR_RES; 371 | area.y2 = LV_VER_RES; 372 | 373 | lv_obj_invalidate_area(lv_scr_act(), &area); // Invalidate this region on the active screen 374 | } 375 | 376 | void RemoteDisplay::readRemoteTouch(lv_indev_t *indev_driver, lv_indev_data_t *data) { 377 | uint8_t remoteStatus = 0; 378 | static lv_indev_state_t lastRemoteTouchState = LV_INDEV_STATE_REL; 379 | static uint16_t lastRemoteTouchX = 0; 380 | static uint16_t lastRemoteTouchY = 0; 381 | char incomingPacket[5]; // 1 byte status + 2x 16-bit integers for X and Y 382 | int packetSize = udp.parsePacket(); 383 | if (packetSize == 5) 384 | { // We expect a 5-byte packet 385 | udp.read(incomingPacket, 5); 386 | 387 | // Extract touch data from packet 388 | remoteStatus = incomingPacket[0]; 389 | int16_t remoteX = (incomingPacket[1] << 8) | incomingPacket[2]; // High byte first 390 | int16_t remoteY = (incomingPacket[3] << 8) | incomingPacket[4]; // High byte first 391 | 392 | switch (remoteStatus) 393 | { 394 | case 0: //Release 395 | lastRemoteTouchState = LV_INDEV_STATE_REL; 396 | break; 397 | case 1: //Touch 398 | lastRemoteTouchState = LV_INDEV_STATE_PR; 399 | lastRemoteTouchX = remoteX; 400 | lastRemoteTouchY = remoteY; 401 | break; 402 | case 2: // Refresh the screen - also used to connect to the remote display 403 | // Get the IP address as a string 404 | char ipStr[16]; 405 | sprintf(ipStr, "%d.%d.%d.%d", udp.remoteIP()[0], udp.remoteIP()[1], udp.remoteIP()[2], udp.remoteIP()[3]); 406 | Serial.printf("Received connect/refresh command from %s\n", ipStr); 407 | 408 | udpAddress = udp.remoteIP(); 409 | transmitInfoPacket(); 410 | refreshDisplay(); 411 | break; 412 | } 413 | } 414 | 415 | data->state = lastRemoteTouchState; 416 | data->point.x = lastRemoteTouchX; 417 | data->point.y = lastRemoteTouchY; 418 | } 419 | 420 | void RemoteDisplay::remoteDispFlush(lv_display_t *disp, const lv_area_t *area, uint8_t *pixelmap) { 421 | sendRLE(area, pixelmap); 422 | lv_disp_flush_ready(disp); 423 | } 424 | -------------------------------------------------------------------------------- /RemoteDisplay.h: -------------------------------------------------------------------------------- 1 | #ifndef REMOTE_DISPLAY_H 2 | #define REMOTE_DISPLAY_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #define MAX_PACKET_SIZE 1430 9 | 10 | class RemoteDisplay { 11 | public: 12 | RemoteDisplay(int udpPort = 24680, const char* ssid = nullptr, const char* password = nullptr); 13 | RemoteDisplay(const char* ssid, const char* password); 14 | void init(lv_display_t *disp); 15 | void send(const lv_area_t *area, uint8_t *pixelmap); 16 | void sendRLE(const lv_area_t *area, uint8_t *pixelmap); 17 | void sendPalettedRLE(const lv_area_t *area, uint8_t *pixelmap); 18 | void remoteDispFlush(lv_display_t *disp, const lv_area_t *area, uint8_t *pixelmap); 19 | void connectRemote(char *ipStr); 20 | void connectRemote(IPAddress ip); 21 | 22 | private: 23 | WiFiUDP udp; 24 | uint32_t udpAddress; 25 | int udpPort; 26 | const char* ssid; 27 | const char* password; 28 | bool wifiConnected; 29 | uint8_t packetBuffer[MAX_PACKET_SIZE]; 30 | uint16_t rleBuffer[MAX_PACKET_SIZE / 2]; 31 | uint8_t infoBuffer[10]; 32 | uint16_t paletteBuffer[16]; 33 | 34 | void readRemoteTouch(lv_indev_t *indev_driver, lv_indev_data_t *data); 35 | void sendRLEPacket(uint16_t transmittedX, uint16_t transmittedY, uint16_t tileWidth, uint16_t progressStart, uint32_t rleLength, uint8_t paletteSize = 0); 36 | void transmitInfoPacket(); 37 | void refreshDisplay(); 38 | }; 39 | 40 | #endif 41 | --------------------------------------------------------------------------------