I previously built an “HA Deck” to simplify building HA dashboards on-device with LVGL. It worked, but I’ve always wanted to try an “RDP-style” path — render in a real browser, stream the result to an ESP32 display.
Remote WebView Server/Client renders your HA dashboard in headless Chromium and streams only the changed parts as JPEG tiles over WebSocket to an ESP32-S3 480×480 RGB screen. The device decodes and blits those tiles directly to the panel, with touch events sent back for scrolling and interaction.
The primary reason to try this approach is a real browser experience (fonts, CSS, charts, components) without re-implementing UI in LVGL.
Check demo:
How it works
Server opens your HA dashboard in headless Chromium
On each screencast frame, it computes diffs, merges adjacent tiles, JPEG-encodes them, and pushes over WS
ESP32-S3 decodes JPEG straight into the display buffer
Touch from the device is forwarded back to the page
Current status
Beta and moving fast. The client firmware is my first attempt with a lot of AI-generated code — good for exploration, but readability/edge-cases still need love.
With 1 - 2 dashboards, it’s smooth; more concurrent pages may need moving CPU-heavy work into worker threads.
Try it?
Server: Docker container (see repo README, use beta tag)
Client: simple ESP32-S3 firmware for a Guition-ESP32-S3-4848S040 (480×480 RGB panel)
Looking for feedback
Would you use this as an always-on HA display? Is an ESPHome component interesting (I plan to prototype and benchmark it vs. the standalone client)?
Ported the client to ESP-IDF. It can render a 480×480 frame as a single tile. Current performance: ~10 ms for partial updates and ~73 ms for full updates.
It could be a bit faster if we split decoding across both cores, but that requires a double buffer, and I haven’t had much luck with that so far.
Drafting an ESPHome external component. Plan to move many settings from the server to the client so different clients can use different configurations.
The only option available right now is to tune the existing code (it’s also 800×480, but 5″) so that it works with your panel. Alternatively, you can wait until the ESPHome component is ready; however:
Your panel must be supported by ESPHome (ESP-IDF framework)
I won’t pursue the ESPHome component path unless it can achieve comparable performance (under 100 ms for a full-screen refresh).
Also, consider that a larger screen will be slower, due to the increased amount of data to transfer/decode.
The updated server and the ESPHome external component have been released. The server repo also includes the Home Assistant add-on files, but I have no way to test whether it works (I’d be surprised if it worked on the first try — if anyone is willing to test and troubleshoot, let me know).
The main updates are:
ESPHome component: Any display supported by ESPHome (and with the new mipi_rgb platform, that’s a lot) can run Remote WebView.
Per-client settings: Each connection can supply its own width, height, tileSize, jpegQuality, maxBytesPerMessage, etc.
Client-driven navigation: The client can control which page to open.
Many quality-of-life improvements.
The built-in self-test shows ~7 ms (~143 FPS) for partial updates and ~58 ms (~17 FPS) for full updates. Note that this does not include data transmission/receipt time.
Is there any chance to support ESP32-P4? I tried to use the config with my ESP32-P4 based display but I got stuck on jpegdec-esphome, since I saw you modified jpegdec specifically for S3…
I don’t have a P4-based screen, but it would be interesting to try (maybe I’ll order one later).
You don’t need JPEGDEC at all, since the ESP32-P4 has a very fast hardware JPEG decoder (Full HD — 48 FPS, according to the docs). It should be integrated into the component for this target (via ifdefs).
Which screen are you using? Is it fully supported by ESPHome?
I’m using this one, which is discounted right now:
I just found this on AliExpress: $125.63 | ESP32P4 Development Board 10.1 Inch 1280*800 Screen WiFi BT Type-C for Arduino LVGL ESP-IDE Smart Home Optional Speaker Battery https://a.aliexpress.com/_mrL6nJx
This is the esphome yaml configuration I’m trying to implement, mixing the working one I was previously using with the remote client sample on your github repo (actually using a fork since your main branch was not working and someone else was able to fork and fix):
When I try to install the esphome yaml config (the one that mixes in your remote webview client) it gives me during linking phase:
Linking .pioenvs/esp32p4tablet/firmware.elf
/data/cache/platformio/packages/toolchain-riscv32-esp/bin/../lib/gcc/riscv32-esp-elf/14.2.0/../../../../riscv32-esp-elf/bin/ld: .pioenvs/esp32p4tablet/src/esphome/components/remote_webview/remote_webview.cpp.o: in function `esphome::remote_webview::RemoteWebView::decode_jpeg_tile_to_lcd_(short, short, unsigned char const*, unsigned int)':
/data/build/esp32p4tablet/src/esphome/components/remote_webview/remote_webview.cpp:294:(.text._ZN7esphome14remote_webview13RemoteWebView24decode_jpeg_tile_to_lcd_EssPKhj+0x2c): undefined reference to `JPEGDEC::openRAM(unsigned char*, int, int (*)(jpeg_draw_tag*))'
/data/cache/platformio/packages/toolchain-riscv32-esp/bin/../lib/gcc/riscv32-esp-elf/14.2.0/../../../../riscv32-esp-elf/bin/ld: /data/build/esp32p4tablet/src/esphome/components/remote_webview/remote_webview.cpp:295:(.text._ZN7esphome14remote_webview13RemoteWebView24decode_jpeg_tile_to_lcd_EssPKhj+0x38): undefined reference to `JPEGDEC::getLastError()'
/data/cache/platformio/packages/toolchain-riscv32-esp/bin/../lib/gcc/riscv32-esp-elf/14.2.0/../../../../riscv32-esp-elf/bin/ld: /data/build/esp32p4tablet/src/esphome/components/remote_webview/remote_webview.cpp:299:(.text._ZN7esphome14remote_webview13RemoteWebView24decode_jpeg_tile_to_lcd_EssPKhj+0x72): undefined reference to `JPEGDEC::setMaxOutputSize(int)'
/data/cache/platformio/packages/toolchain-riscv32-esp/bin/../lib/gcc/riscv32-esp-elf/14.2.0/../../../../riscv32-esp-elf/bin/ld: /data/build/esp32p4tablet/src/esphome/components/remote_webview/remote_webview.cpp:300:(.text._ZN7esphome14remote_webview13RemoteWebView24decode_jpeg_tile_to_lcd_EssPKhj+0x7e): undefined reference to `JPEGDEC::setPixelType(int)'
/data/cache/platformio/packages/toolchain-riscv32-esp/bin/../lib/gcc/riscv32-esp-elf/14.2.0/../../../../riscv32-esp-elf/bin/ld: /data/build/esp32p4tablet/src/esphome/components/remote_webview/remote_webview.cpp:302:(.text._ZN7esphome14remote_webview13RemoteWebView24decode_jpeg_tile_to_lcd_EssPKhj+0x8e): undefined reference to `JPEGDEC::decode(int, int, int)'
/data/cache/platformio/packages/toolchain-riscv32-esp/bin/../lib/gcc/riscv32-esp-elf/14.2.0/../../../../riscv32-esp-elf/bin/ld: /data/build/esp32p4tablet/src/esphome/components/remote_webview/remote_webview.cpp:303:(.text._ZN7esphome14remote_webview13RemoteWebView24decode_jpeg_tile_to_lcd_EssPKhj+0x9a): undefined reference to `JPEGDEC::getLastError()'
/data/cache/platformio/packages/toolchain-riscv32-esp/bin/../lib/gcc/riscv32-esp-elf/14.2.0/../../../../riscv32-esp-elf/bin/ld: /data/build/esp32p4tablet/src/esphome/components/remote_webview/remote_webview.cpp:304:(.text._ZN7esphome14remote_webview13RemoteWebView24decode_jpeg_tile_to_lcd_EssPKhj+0xc2): undefined reference to `JPEGDEC::close()'
/data/cache/platformio/packages/toolchain-riscv32-esp/bin/../lib/gcc/riscv32-esp-elf/14.2.0/../../../../riscv32-esp-elf/bin/ld: /data/build/esp32p4tablet/src/esphome/components/remote_webview/remote_webview.cpp:305:(.text._ZN7esphome14remote_webview13RemoteWebView24decode_jpeg_tile_to_lcd_EssPKhj+0xce): undefined reference to `JPEGDEC::close()'
/data/cache/platformio/packages/toolchain-riscv32-esp/bin/../lib/gcc/riscv32-esp-elf/14.2.0/../../../../riscv32-esp-elf/bin/ld: .pioenvs/esp32p4tablet/src/main.cpp.o: in function `std::_Function_handler<void (std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >), setup()::{lambda(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)#1}>::_M_invoke(std::_Any_data const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&)':
/config/esphome/esp32p4tablet.yaml:131:(.text._ZNSt17_Function_handlerIFvNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEEZ5setupvEUlS5_E_E9_M_invokeERKSt9_Any_dataOS5_+0x4c): undefined reference to `esphome::remote_webview::RemoteWebView::open_url(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
Which I believe it is due to the usage of your modified jpeg library that you made specifically for S3 right? If I use the built-in one provided by ESP-IDF instead, do you know if your code would link into that too, or that would require modifications in the code?
Just heads up: I made compilation+linking work with the original bitbank2/[email protected] library. Still fixing some stuff but will post the config and results soon. Cross fingers
Yeah, the modified fork was only for the ESP32-S3. I’ve already fixed the build issues (you can try loading RWV from the original repo) and added more targets (including P4) to jpegdec-esphome.
Without SIMD, rendering will be slow, but the P4’s faster CPU can compensate.