ESP8266 zeigt Webseiten-Screenshot als RAW-Bild auf TFT-Display

📸 Webseiten-Screenshot direkt aufs Display – mit ESP8266

Nach zahlreichen Irrwegen, halbfertigen Internet-Beispielen und einer kleinen Odyssee durch Puppeteer, ffmpeg und die ESP-Hölle steht sie endlich: Eine komplett funktionierende Lösung, um eine Webseite als Bild zu screenshotten, in ein ESP-freundliches RAW-Format zu wandeln und auf einem ST7789-Display darzustellen.


🔧 Was du brauchst (Server-Seite)

  • Ein Server mit Linux (getestet auf Ubuntu)
  • nodejs, npm, chromium, imagemagick, ffmpeg
  • Puppeteer (NodeJS-Modul)
sudo apt update
sudo apt install -y nodejs npm chromium imagemagick ffmpeg
npm install puppeteer

Tipp: Falls Puppeteer nicht sein eigenes Chromium laden soll:

PUPPETEER_SKIP_DOWNLOAD=true npm install puppeteer

📂 Dateistruktur auf dem Server

/var/www/html/
├── bmp.php
├── screenshot.js
├── screenshot_small.png  (autogeneriert)
├── screenshot_big.png    (autogeneriert)
/tmp/
├── tmp_small.png         (temp)
├── tmp_big.png           (temp)
├── bild.raw              (fertig für ESP)

🧠 Das Backend – bmp.php

Dieses PHP-Skript ruft screenshot.js auf, erzeugt den Screenshot und liefert je nach Parameter ein BMP oder ein rgb565-RAW-File.

<?php
$seite = $_GET['seite'] ?? null;
$format = $_GET['format'] ?? 'bmp';

if (!$seite) {
    header("HTTP/1.1 400 Bad Request");
    echo "Seite fehlt!";
    exit;
}

$command = "node /var/www/html/screenshot.js '$seite' 2>&1";
exec($command, $output, $return_var);

if ($return_var !== 0) {
    header("HTTP/1.1 500 Internal Server Error");
    echo "Fehler beim Screenshot-Erzeugen: " . implode("\n", $output);
    exit;
}

$pngFile = "/var/www/html/screenshot_small.png";
if (!file_exists($pngFile)) {
    header("HTTP/1.1 500 Internal Server Error");
    echo "Screenshot wurde nicht erzeugt!";
    exit;
}

if ($format === 'raw') {
    $rawFile = "/tmp/bild.raw";
    $convertCmd = "ffmpeg -y -i '$pngFile' -vf scale=320:240 -f rawvideo -pix_fmt rgb565 '$rawFile' 2>&1";
    exec($convertCmd, $ffmpeg_output, $ffmpeg_return);

    if ($ffmpeg_return !== 0 || !file_exists($rawFile)) {
        header("HTTP/1.1 500 Server Error");
        echo "RAW-Bild konnte nicht erstellt werden!\n";
        echo implode("\n", $ffmpeg_output);
        exit;
    }

    header("Content-Type: application/octet-stream");
    header("Content-Disposition: inline; filename=\"bild.raw\"");
    readfile($rawFile);
    exit;
} else {
    $bmpFile = "/tmp/ausgabe.bmp";
    if (file_exists($bmpFile)) {
        header("Content-Type: image/bmp");
        header("Content-Disposition: inline; filename=\"bild.bmp\"");
        readfile($bmpFile);
        exit;
    } else {
        header("HTTP/1.1 500 Server Error");
        echo "BMP nicht gefunden!";
        exit;
    }
}
?>

📸 Der Screenshot-Macher – screenshot.js

const puppeteer = require('puppeteer');
const fs = require('fs');
const { exec } = require('child_process');

(async () => {
  const url = process.argv[2];
  const format = (process.argv[3] || 'png').toLowerCase();
  if (!url) process.exit(1);

  const tmpBigPng = "/tmp/tmp_big.png";
  const tmpSmallPng = "/tmp/tmp_small.png";
  const bigOut = `/var/www/html/screenshot_big.${format}`;
  const smallOut = `/var/www/html/screenshot_small.${format}`;

  const browser = await puppeteer.launch({
    headless: "new",
    executablePath: '/usr/bin/chromium',
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  const page = await browser.newPage();
  await page.setViewport({ width: 800, height: 480 });
  await page.goto(url, { waitUntil: 'networkidle2' });
  await page.screenshot({ path: tmpBigPng });

  await page.setViewport({ width: 320, height: 240, deviceScaleFactor: 2 });
  await page.goto(url, { waitUntil: 'networkidle2' });
  await page.screenshot({ path: tmpSmallPng });
  await browser.close();

  const convert = (input, output, width, height, callback) => {
    if (format === "bmp") {
      const cmd = `convert ${input} -resize ${width}x${height}! -depth 8 -colors 256 -alpha off -type Palette -define bmp:format=bmp3 ${output}`;
      exec(cmd, (err) => {
        if (err) process.exit(1);
        fs.unlinkSync(input);
        if (callback) callback();
      });
    } else {
      fs.copyFileSync(input, output);
      fs.unlinkSync(input);
      if (callback) callback();
    }
  };

  convert(tmpBigPng, bigOut, 800, 480, () => {
    convert(tmpSmallPng, smallOut, 320, 240, () => {
      console.log("🎉 Screenshots fertig gespeichert!");
    });
  });
})();

📟 ESP8266-Code zum Anzeigen des RAW-Bildes

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SPI.h>

#define TFT_CS   5
#define TFT_RST  2
#define TFT_DC   4
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);

const char* ssid = "DEIN_NETZ";
const char* password = "GEHEIM";
const char* url = "http://DEIN_SERVER/bmp.php?seite=http://ZIELSEITE&format=raw";

#define IMG_W 320
#define IMG_H 240

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);

  tft.init(240, 320);
  tft.setRotation(1);
  tft.fillScreen(ST77XX_BLACK);
  showImageFromURL(url);
}

void loop() {}

void showImageFromURL(const char* url) {
  WiFiClient client;
  HTTPClient http;
  http.begin(client, url);
  int code = http.GET();

  if (code == HTTP_CODE_OK) {
    WiFiClient *stream = http.getStreamPtr();
    static uint16_t lineBuf[IMG_W];
    tft.startWrite();
    tft.setAddrWindow(0, 0, IMG_W, IMG_H);

    for (uint16_t y = 0; y < IMG_H; y++) {
      for (uint16_t x = 0; x < IMG_W; x++) {
        while (stream->available() < 2) delay(1);
        uint8_t hi = stream->read();
        uint8_t lo = stream->read();
        lineBuf[x] = (hi << 8) | lo;
      }
      tft.writePixels(lineBuf, IMG_W, true);
    }
    tft.endWrite();
  }
  http.end();
}

🎬 Ergebnis

So sieht’s am Ende aus – direkt vom ESP8266 auf dem ST7789:

(Foto einfügen optional – getestet am 03.08.2025, 14:31)


🧠 Fazit

Ja, es war ein büschen Tüftelei. Aber jetzt steht’s wie ‘n Leuchtturm bei Windstärke 8.

Falls du dir mal ’nen Screenshot auf dein ESP-Display holen willst: So geht’s. Nicht hübsch drumrumgeredet – sondern einfach nur: läuft.

Nach oben scrollen