Zum Inhalt

INVERTER-ESP Browser Simulator

Zweck: Live-Demo des E-Paper Displays im Browser fรผr API-Testing ohne Hardware
Zielgruppe: API-Entwickler, Demo fรผr Stakeholder
Status: ๐Ÿ“‹ Konzept (nicht implementiert)


๐ŸŽฏ Anforderungen

Must Have:

  • โœ… Zeigt gleiches Layout wie E-Paper Display (296ร—128 Monochrome)
  • โœ… 4 Screens: Dashboard, History, SysInfo, Settings
  • โœ… Navigation mit Buttons (1=Next, 2=Prev, 3=Refresh)
  • โœ… Langsamer Refresh (30 Sekunden wie E-Paper)
  • โœ… Live-Daten von API (Demo-Daten)
  • โœ… Nicht-interaktiv auรŸer Blรคttern

Nice to Have:

  • โธ๏ธ E-Paper "Ghosting" Effect Simulation
  • โธ๏ธ Screen-Transition Animation (fade to white, then new screen)
  • โธ๏ธ Auto-Rotation durch alle Screens

๐Ÿ”ง Technische Optionen

Option 1: LVGL WASM (Native LVGL im Browser) ๐Ÿ† EMPFOHLEN

Vorteile: - โœ… Identischer Code wie ESP32 Firmware - โœ… Pixel-perfektes Rendering - โœ… LVGL monochrome Modus funktioniert - โœ… Einfach zu maintainen (eine Codebase)

Nachteile: - โš ๏ธ Komplexe Build-Pipeline (Emscripten) - โš ๏ธ ~2-3 Tage Setup-Zeit - โš ๏ธ LVGL muss mit Emscripten kompiliert werden

Technologie:

C/C++ (LVGL) โ†’ Emscripten โ†’ WASM โ†’ Browser

Beispiel: https://docs.lvgl.io/master/get-started/platforms/web.html


Option 2: HTML5 Canvas Nachbau (Einfach & Schnell) โšก

Vorteile: - โœ… Schnell implementiert (1-2 Tage) - โœ… Keine WASM-Komplexitรคt - โœ… Einfach in API-Server integrierbar - โœ… Responsive Design mรถglich

Nachteile: - โš ๏ธ Nicht identisch mit ESP32 Code - โš ๏ธ Manuelles Layout-Nachbauen - โš ๏ธ Zwei Codebases zu maintainen

Technologie:

HTML5 Canvas โ†’ JavaScript โ†’ Fetch API โ†’ Backend


Option 3: SVG/CSS Nachbau (Einfachste Variante) ๐Ÿš€

Vorteile: - โœ… Sehr schnell (4-6 Stunden) - โœ… Kein Canvas-Rendering nรถtig - โœ… Responsive & skalierbar - โœ… Einfach zu stylen

Nachteile: - โš ๏ธ Sieht nicht wie E-Paper aus - โš ๏ธ Kein pixel-perfektes Rendering - โš ๏ธ Zwei Codebases

Technologie:

HTML/CSS/JS โ†’ Fetch API โ†’ Backend


๐Ÿ† Empfehlung: Option 2 (Canvas Nachbau)

Grund: Beste Balance zwischen Aufwand und Ergebnis fรผr API-Demo

Warum nicht Option 1 (LVGL WASM)?

  • Zu komplex fรผr eine Demo
  • 2-3 Tage Setup vs 1-2 Tage fertige Demo
  • ESP32 Code ist noch nicht fertig (keine UI zu kompilieren)

Warum nicht Option 3 (SVG/CSS)?

  • Zu weit von realer Hardware entfernt
  • Wirkt nicht wie E-Paper Display
  • Fรผr Demo wichtig: "So sieht es auf dem Device aus"

๐Ÿ“‹ Implementation Plan (Option 2: Canvas)

Phase 1: Basic Canvas Setup (2-3 Stunden)

<!DOCTYPE html>
<html>
<head>
    <title>INVERTER-ESP E-Paper Simulator</title>
    <style>
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background: #333;
            font-family: 'Courier New', monospace;
        }

        #simulator-container {
            background: #e0e0e0;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 50px rgba(0,0,0,0.5);
        }

        #epaper-canvas {
            background: white;
            border: 2px solid #666;
            image-rendering: pixelated;
            /* 296ร—128 scaled 3x = 888ร—384 */
            width: 888px;
            height: 384px;
        }

        .controls {
            margin-top: 20px;
            text-align: center;
        }

        button {
            background: #444;
            color: white;
            border: none;
            padding: 15px 30px;
            margin: 0 10px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
        }

        button:hover {
            background: #666;
        }

        button:active {
            background: #222;
        }

        .status {
            margin-top: 10px;
            color: #ccc;
            font-size: 12px;
        }
    </style>
</head>
<body>
    <div id="simulator-container">
        <canvas id="epaper-canvas" width="296" height="128"></canvas>
        <div class="controls">
            <button onclick="prevScreen()">โ—€ Prev (2)</button>
            <button onclick="refreshData()">๐Ÿ”„ Refresh (3)</button>
            <button onclick="nextScreen()">Next (1) โ–ถ</button>
        </div>
        <div class="status">
            <span id="current-screen">Screen: Dashboard</span> | 
            <span id="last-update">Updated: --:--</span> | 
            <span id="next-refresh">Next: 30s</span>
        </div>
    </div>

    <script src="epaper-simulator.js"></script>
</body>
</html>

Phase 2: Canvas Rendering Engine (3-4 Stunden)

// epaper-simulator.js

class EPaperSimulator {
    constructor(canvasId, apiBaseUrl) {
        this.canvas = document.getElementById(canvasId);
        this.ctx = this.canvas.getContext('2d');
        this.apiBaseUrl = apiBaseUrl;

        this.width = 296;
        this.height = 128;

        this.currentScreen = 0;
        this.screens = ['dashboard', 'history', 'sysinfo', 'settings'];
        this.screenNames = ['Dashboard', 'History', 'System Info', 'Settings'];

        this.data = null;
        this.lastUpdate = null;
        this.refreshInterval = 30000; // 30 seconds

        this.init();
    }

    init() {
        // White background (E-Paper style)
        this.ctx.fillStyle = 'white';
        this.ctx.fillRect(0, 0, this.width, this.height);

        // Start data fetching
        this.fetchData();
        setInterval(() => this.fetchData(), this.refreshInterval);

        // Keyboard support
        document.addEventListener('keydown', (e) => {
            if (e.key === '1') this.nextScreen();
            if (e.key === '2') this.prevScreen();
            if (e.key === '3') this.refreshData();
        });

        this.render();
    }

    async fetchData() {
        try {
            const response = await fetch(`${this.apiBaseUrl}/api/inverter/status`);
            this.data = await response.json();
            this.lastUpdate = new Date();
            this.updateStatus();
            this.render();
        } catch (error) {
            console.error('API fetch failed:', error);
        }
    }

    render() {
        // Clear canvas (E-Paper refresh effect)
        this.ctx.fillStyle = 'white';
        this.ctx.fillRect(0, 0, this.width, this.height);

        // Render current screen
        switch (this.screens[this.currentScreen]) {
            case 'dashboard':
                this.renderDashboard();
                break;
            case 'history':
                this.renderHistory();
                break;
            case 'sysinfo':
                this.renderSysInfo();
                break;
            case 'settings':
                this.renderSettings();
                break;
        }
    }

    renderDashboard() {
        if (!this.data) return;

        // Title
        this.drawText('INVERTER-ESP', 148, 15, 16, 'center');

        // Power (large)
        this.drawText(`${this.data.power} kW`, 148, 50, 32, 'center', 'bold');

        // Energy today
        this.drawText(`Today: ${this.data.energyToday} kWh`, 148, 80, 14, 'center');

        // Status
        const statusText = this.data.producing ? 'Producing' : 'Idle';
        this.drawText(statusText, 148, 100, 14, 'center');

        // Footer
        this.drawText('Press 1 for History', 148, 120, 10, 'center');
    }

    renderHistory() {
        // Title
        this.drawText('7-Day History', 148, 15, 14, 'center');

        if (!this.data || !this.data.history) return;

        // Bar chart
        const barWidth = 30;
        const barSpacing = 10;
        const maxHeight = 70;
        const baseY = 110;

        const maxValue = Math.max(...this.data.history.map(d => d.energy));

        this.data.history.forEach((day, i) => {
            const x = 20 + i * (barWidth + barSpacing);
            const height = (day.energy / maxValue) * maxHeight;
            const y = baseY - height;

            // Bar
            this.ctx.fillStyle = 'black';
            this.ctx.fillRect(x, y, barWidth, height);

            // Day label
            this.drawText(day.label, x + barWidth/2, baseY + 15, 10, 'center');

            // Value
            this.drawText(`${day.energy}`, x + barWidth/2, y - 5, 8, 'center');
        });
    }

    renderSysInfo() {
        this.drawText('System Info', 148, 15, 14, 'center');

        if (!this.data) return;

        let y = 35;
        const lineHeight = 18;

        this.drawText(`WiFi: ${this.data.wifi.ssid}`, 10, y, 12); y += lineHeight;
        this.drawText(`IP: ${this.data.wifi.ip}`, 10, y, 12); y += lineHeight;
        this.drawText(`API: ${this.data.api.status}`, 10, y, 12); y += lineHeight;
        this.drawText(`Uptime: ${this.data.system.uptime}`, 10, y, 12); y += lineHeight;
        this.drawText(`Memory: ${this.data.system.freeMemory} KB`, 10, y, 12);
    }

    renderSettings() {
        this.drawText('Settings', 148, 15, 14, 'center');

        const items = [
            '> WiFi Setup',
            '  API Config',
            '  Update Interval',
            '  Display Contrast',
            '  Firmware Update',
            '  Factory Reset'
        ];

        let y = 35;
        items.forEach(item => {
            this.drawText(item, 10, y, 12);
            y += 15;
        });
    }

    drawText(text, x, y, size, align = 'left', weight = 'normal') {
        this.ctx.fillStyle = 'black';
        this.ctx.font = `${weight} ${size}px monospace`;
        this.ctx.textAlign = align;
        this.ctx.textBaseline = 'top';
        this.ctx.fillText(text, x, y);
    }

    nextScreen() {
        this.currentScreen = (this.currentScreen + 1) % this.screens.length;
        this.updateStatus();
        this.render();
    }

    prevScreen() {
        this.currentScreen = (this.currentScreen - 1 + this.screens.length) % this.screens.length;
        this.updateStatus();
        this.render();
    }

    refreshData() {
        this.fetchData();
    }

    updateStatus() {
        document.getElementById('current-screen').textContent = 
            `Screen: ${this.screenNames[this.currentScreen]}`;

        if (this.lastUpdate) {
            const time = this.lastUpdate.toLocaleTimeString();
            document.getElementById('last-update').textContent = `Updated: ${time}`;
        }
    }
}

// Initialize simulator
const simulator = new EPaperSimulator('epaper-canvas', 'http://localhost:8090');

Phase 3: API Integration (1-2 Stunden)

API Endpoint benรถtigt:

GET /api/inverter/status

Response:
{
  "power": 3.5,
  "energyToday": 12.5,
  "producing": true,
  "history": [
    {"label": "Mo", "energy": 18.3},
    {"label": "Tu", "energy": 22.1},
    {"label": "We", "energy": 19.8},
    {"label": "Th", "energy": 21.5},
    {"label": "Fr", "energy": 20.2},
    {"label": "Sa", "energy": 23.7},
    {"label": "Su", "energy": 12.5}
  ],
  "wifi": {
    "ssid": "HomeWiFi",
    "ip": "192.168.1.100",
    "rssi": -45
  },
  "api": {
    "status": "Connected",
    "lastSync": "2025-10-23T14:30:00Z"
  },
  "system": {
    "uptime": "2d 5h 23m",
    "freeMemory": 234
  }
}

Phase 4: E-Paper Effekte (Optional, 2-3 Stunden)

// Add ghosting effect (simulate E-Paper partial refresh)
renderWithGhosting() {
    // Store old screen
    const oldData = this.ctx.getImageData(0, 0, this.width, this.height);

    // Flash white (full refresh simulation)
    this.ctx.fillStyle = 'white';
    this.ctx.fillRect(0, 0, this.width, this.height);

    setTimeout(() => {
        // Show ghosted old screen (30% opacity)
        this.ctx.globalAlpha = 0.3;
        this.ctx.putImageData(oldData, 0, 0);
        this.ctx.globalAlpha = 1.0;

        setTimeout(() => {
            // Render new screen
            this.render();
        }, 100);
    }, 100);
}

// Add slow refresh animation
async refreshWithAnimation() {
    // Fade to white
    for (let alpha = 0; alpha <= 1; alpha += 0.1) {
        this.ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
        this.ctx.fillRect(0, 0, this.width, this.height);
        await this.sleep(50);
    }

    // Fetch new data
    await this.fetchData();

    // Fade in new screen
    this.render();
}

sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

๐Ÿš€ Deployment

Im API-Server integrieren

Ordnerstruktur:

api-server/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ routes/
โ”‚   โ”‚   โ””โ”€โ”€ inverter.ts        API endpoints
โ”‚   โ””โ”€โ”€ public/
โ”‚       โ”œโ”€โ”€ simulator/
โ”‚       โ”‚   โ”œโ”€โ”€ index.html     Simulator UI
โ”‚       โ”‚   โ”œโ”€โ”€ epaper-simulator.js
โ”‚       โ”‚   โ””โ”€โ”€ styles.css
โ”‚       โ””โ”€โ”€ index.html         Landing page

URL: http://localhost:8090/simulator/

Als standalone HTML

Einfach die 3 Dateien รถffnen: - index.html - epaper-simulator.js - styles.css

Funktioniert auch ohne Server (CORS beachten fรผr API calls).


โœ… Checkliste fรผr anderen Agent

Must Do:

  • API Endpoint /api/inverter/status erstellen
  • Demo-Daten generieren (mock solar data)
  • CORS headers setzen (fรผr lokales Testen)
  • 30-Sekunden Refresh implementieren (Backend)

Should Do:

  • Canvas Simulator HTML/JS erstellen (siehe oben)
  • In API-Server integrieren (/simulator/ route)
  • Keyboard shortcuts testen (1/2/3)
  • Responsive Design (optional)

Nice to Have:

  • E-Paper Ghosting Effekt
  • Auto-Rotation durch Screens
  • Screenshot-Funktion
  • Download als PNG

๐Ÿ“ Vorbereitete Dateien

Ich kann folgendes vorbereiten:

  1. โœ… simulator/web/index.html - Complete HTML structure
  2. โœ… simulator/web/epaper-simulator.js - Full JavaScript implementation
  3. โœ… simulator/web/styles.css - Styling (optional, inline mรถglich)
  4. โœ… simulator/web/README.md - Integration guide
  5. โธ๏ธ API Endpoint Spec - fรผr anderen Agent (JSON format)

๐ŸŽฏ Zeitaufwand

Minimal (ohne Effekte): - HTML/CSS: 1 Stunde - JavaScript: 3 Stunden - API Integration: 1 Stunde - Testing: 1 Stunde Total: ~6 Stunden

Mit E-Paper Effekten: - + Ghosting Effect: 2 Stunden - + Animations: 1 Stunde - + Polish: 1 Stunde Total: ~10 Stunden


๐Ÿค Agent Handover

Fรผr API Agent: 1. Erstelle /api/inverter/status endpoint 2. Returniere JSON mit Demo-Daten (siehe Schema oben) 3. Update alle 30 Sekunden mit neuen mock values 4. Setze CORS headers

Fรผr mich (Simulator Agent): 1. Erstelle HTML/JS Files 2. Test mit mock API 3. Integration guide schreiben 4. Screenshot fรผr README


๐Ÿ“„ Nรคchste Schritte

Soll ich vorbereiten?: - [ ] Complete HTML/CSS/JS files erstellen? - [ ] Mock API spec fรผr anderen Agent? - [ ] Integration guide schreiben? - [ ] Quick-Start README?

Oder soll der andere Agent selbst implementieren? โ†’ Dann nur API spec und Requirements dokumentieren


Status: ๐Ÿ“‹ Konzept fertig, warte auf Entscheidung
Empfehlung: Ich erstelle die Files, anderer Agent integriert in seinen API-Server