ionpy:project_rodemap
no way to compare when less than two revisions
Differences
This shows you the differences between two versions of the page.
| — | ionpy:project_rodemap [2026/02/28 09:36] (current) – created dominik | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| + | ====== ionpy Pro OS — Entwicklungs-Fahrplan & Opus-Einstieg ====== | ||
| + | Erstellt: 2026-02-28\\ | ||
| + | Basis: Architektur-Analyse & Konzept-Session mit Claude Sonnet\\ | ||
| + | Nächster Schritt: Umsetzung mit Claude Opus | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Inhaltsverzeichnis ===== | ||
| + | |||
| + | - Kontext & Ausgangslage | ||
| + | - Was in dieser Session entschieden wurde | ||
| + | - Gesamtarchitektur (Zielzustand) | ||
| + | - Fahrplan: Phase für Phase | ||
| + | - Prompts für Claude Opus | ||
| + | - Offene Fragen & Entscheidungen | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ====== 1. Kontext & Ausgangslage ====== | ||
| + | |||
| + | ionpy ist ein Python/ | ||
| + | |||
| + | < | ||
| + | Transport → Framing → Device (Entity-ORM) → EventBus → WebSocket → Vue Frontend | ||
| + | </ | ||
| + | |||
| + | **Was bereits sehr gut funktioniert: | ||
| + | * Deklaratives Entity-ORM (vergleichbar Django Models) | ||
| + | * Dynamisches Treiber-Loading via importlib | ||
| + | * Transport/ | ||
| + | * GridStack-basiertes Widget-Dashboard | ||
| + | * uPlot Charts mit Plugin-Architektur | ||
| + | * Workspace-Serialisierung (JSON, pro Widget exportState()) | ||
| + | |||
| + | **Was fehlt und gebaut werden muss:** | ||
| + | * Projekt-Konzept als übergeordnete Klammer | ||
| + | * Persistenz (SQLite, strukturiert) | ||
| + | * Device-Anlegen über die UI (aktuell nur YAML) | ||
| + | * Alarm-Routing (Berechnung läuft, aber nirgendwo hin) | ||
| + | * Session-Management (existiert, aber nicht wirklich genutzt) | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ====== 2. Was in dieser Session entschieden wurde ====== | ||
| + | |||
| + | ===== 2.1 Das Projekt-Konzept ===== | ||
| + | |||
| + | Die drei bisher getrennten Systeme — Config (YAML), Session (Backend), Workspace (Frontend) — werden zu einem **Projekt** zusammengefasst. | ||
| + | |||
| + | Ein Projekt ist ein Experiment-Container. Alles was zu einem Messaufbau gehört, lebt zusammen: | ||
| + | |||
| + | < | ||
| + | projects/ | ||
| + | └── akku-charakterisierung/ | ||
| + | ├── project.json | ||
| + | ├── devices.json | ||
| + | ├── workspace.json | ||
| + | ├── data.db | ||
| + | ├── scripts/ | ||
| + | │ | ||
| + | └── exports/ | ||
| + | └── PSU_01_session1.csv | ||
| + | </ | ||
| + | |||
| + | ===== 2.2 Die JSON-Strukturen ===== | ||
| + | |||
| + | ==== project.json ==== | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | " | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **Warum schema_version? | ||
| + | **Warum UUID als id?** "Copy devices from Project XY" referenziert über ID, nicht über Namen.\\ | ||
| + | **Warum files-Block? | ||
| + | |||
| + | ==== devices.json ==== | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | ] | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **Wichtige Entscheidungen in dieser Struktur:** | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | ===== 2.3 Das Datenbank-Schema ===== | ||
| + | |||
| + | **Entscheidung: | ||
| + | |||
| + | Begründung: | ||
| + | * Eine DB → Session-übergreifende Abfragen ohne JOIN über Dateien | ||
| + | * Eine Tabelle pro Device → direkte Spalten statt EAV oder JSON-Blobs | ||
| + | * Jede Spalte = eine Entity → maximal einfache, schnelle Abfragen | ||
| + | * Dynamisches Schema → Spalten werden beim Treiber-Start angelegt/ | ||
| + | |||
| + | <code sql> | ||
| + | -- Wird beim ersten Start des Projekts angelegt | ||
| + | CREATE TABLE sessions ( | ||
| + | id TEXT PRIMARY KEY, | ||
| + | name TEXT NOT NULL, | ||
| + | created | ||
| + | stopped | ||
| + | status | ||
| + | conditions | ||
| + | notes TEXT | ||
| + | ); | ||
| + | |||
| + | -- Zeitmarker für Chart-Annotation und Daten-Segmentierung | ||
| + | CREATE TABLE markers ( | ||
| + | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| + | timestamp | ||
| + | session_id | ||
| + | device_id | ||
| + | label TEXT NOT NULL, -- z.B. " | ||
| + | color TEXT DEFAULT '# | ||
| + | notes TEXT | ||
| + | ); | ||
| + | |||
| + | -- Alarm-Verlauf | ||
| + | CREATE TABLE alarms ( | ||
| + | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| + | timestamp | ||
| + | session_id | ||
| + | device_id | ||
| + | entity_id | ||
| + | old_state | ||
| + | new_state | ||
| + | value REAL, | ||
| + | acknowledged INTEGER DEFAULT 0, | ||
| + | ack_time | ||
| + | ack_by | ||
| + | ); | ||
| + | |||
| + | -- Export-Historie | ||
| + | CREATE TABLE exports ( | ||
| + | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| + | created | ||
| + | session_id | ||
| + | format | ||
| + | filename | ||
| + | device_id | ||
| + | entity_id | ||
| + | ); | ||
| + | |||
| + | -- Device-Tabellen werden DYNAMISCH angelegt: | ||
| + | -- CREATE TABLE device_PSU_01 ( | ||
| + | -- | ||
| + | -- | ||
| + | -- | ||
| + | -- | ||
| + | -- | ||
| + | -- | ||
| + | -- | ||
| + | -- | ||
| + | -- ); | ||
| + | -- CREATE INDEX idx_PSU_01 ON device_PSU_01(session_id, | ||
| + | </ | ||
| + | |||
| + | **Abfrage-Beispiele: | ||
| + | |||
| + | <code sql> | ||
| + | -- Chart: 100 Spannungswerte einer Session | ||
| + | SELECT timestamp, u_out, i_out | ||
| + | FROM device_PSU_01 | ||
| + | WHERE session_id = ' | ||
| + | AND timestamp BETWEEN 1706000000 AND 1706003600 | ||
| + | ORDER BY timestamp; | ||
| + | |||
| + | -- Session-Vergleich: | ||
| + | SELECT session_id, MAX(u_out), MIN(u_out), AVG(i_out) | ||
| + | FROM device_PSU_01 | ||
| + | GROUP BY session_id; | ||
| + | |||
| + | -- Daten zwischen zwei Markern (Lade-Segment) | ||
| + | SELECT d.timestamp, | ||
| + | FROM device_PSU_01 d | ||
| + | WHERE d.session_id = ' | ||
| + | AND d.timestamp BETWEEN | ||
| + | (SELECT timestamp FROM markers WHERE label=' | ||
| + | AND | ||
| + | (SELECT timestamp FROM markers WHERE label=' | ||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ====== 3. Gesamtarchitektur (Zielzustand) ====== | ||
| + | |||
| + | < | ||
| + | ┌─────────────────────────────────────────────────────────────┐ | ||
| + | │ Vue 3 Frontend | ||
| + | │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐ | ||
| + | │ │ Projekt- │ │ Device- | ||
| + | │ │ Manager | ||
| + | │ └──────────┘ └──────────┘ └──────────┘ └───────────────┘ | ||
| + | │ globalStore (reactive) | ||
| + | │ │ WebSocket | ||
| + | ├─────────────────────────┼───────────────────────────────────┤ | ||
| + | │ FastAPI Server | ||
| + | │ ┌──────────────┐ | ||
| + | │ │ REST API │ │ WS Route │ │ Static Files │ │ | ||
| + | │ │ / | ||
| + | │ └──────┬───────┘ | ||
| + | │ | ||
| + | │ SystemEngine | ||
| + | │ ┌──────────────┬───────────────┬────────────────┐ | ||
| + | │ ProjectManager | ||
| + | │ │ │ | ||
| + | │ project.json | ||
| + | │ sessions | ||
| + | │ │ | ||
| + | │ ProjectDatabase (SQLite) | ||
| + | │ ├── sessions | ||
| + | │ ├── markers | ||
| + | │ ├── alarms | ||
| + | │ └── device_XXX (dynamisch) | ||
| + | └─────────────────────────────────────────────────────────────┘ | ||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ====== 4. Fahrplan: Phase für Phase ====== | ||
| + | |||
| + | ===== Phase 1: SQLite Backend (Fundament) ===== | ||
| + | |||
| + | **Warum zuerst?** Ohne Persistenz ergibt das Projekt-Konzept keinen Sinn. Die DB-Struktur muss stehen bevor der ProjectManager gebaut wird, weil '' | ||
| + | |||
| + | **Geschätzter Aufwand: 2 Tage** | ||
| + | |||
| + | ^ Aufgabe ^ Datei ^ Details ^ | ||
| + | | ProjectDatabase Klasse | '' | ||
| + | | ensure_device_table() | '' | ||
| + | | Storage Worker in Engine | '' | ||
| + | | Marker-API | '' | ||
| + | | History REST Endpoint | '' | ||
| + | |||
| + | **Kern-Klasse: | ||
| + | |||
| + | <code python> | ||
| + | # core/ | ||
| + | |||
| + | import aiosqlite | ||
| + | import asyncio | ||
| + | import logging | ||
| + | import time | ||
| + | import os | ||
| + | from typing import Optional | ||
| + | |||
| + | class ProjectDatabase: | ||
| + | """ | ||
| + | Eine SQLite-Datenbank pro Projekt. | ||
| + | Eine Tabelle pro Device, Spalten = Entities. | ||
| + | Dynamisches Schema-Management. | ||
| + | """ | ||
| + | | ||
| + | def __init__(self, | ||
| + | self.db_path = os.path.join(project_path, | ||
| + | self._conn: Optional[aiosqlite.Connection] = None | ||
| + | self._batch: | ||
| + | self._flush_interval = 0.5 | ||
| + | self.logger = logging.getLogger(" | ||
| + | | ||
| + | async def initialize(self): | ||
| + | self._conn = await aiosqlite.connect(self.db_path) | ||
| + | self._conn.row_factory = aiosqlite.Row | ||
| + | # WAL-Mode: bessere Concurrent-Performance, | ||
| + | await self._conn.execute(" | ||
| + | await self._conn.execute(" | ||
| + | await self._create_base_schema() | ||
| + | asyncio.create_task(self._flush_loop()) | ||
| + | self.logger.info(f" | ||
| + | | ||
| + | async def _create_base_schema(self): | ||
| + | await self._conn.executescript(""" | ||
| + | CREATE TABLE IF NOT EXISTS sessions ( | ||
| + | id TEXT PRIMARY KEY, | ||
| + | name TEXT NOT NULL, | ||
| + | created | ||
| + | stopped | ||
| + | status | ||
| + | conditions TEXT, | ||
| + | notes TEXT | ||
| + | ); | ||
| + | CREATE TABLE IF NOT EXISTS markers ( | ||
| + | id | ||
| + | timestamp | ||
| + | session_id TEXT NOT NULL, | ||
| + | device_id | ||
| + | label TEXT NOT NULL, | ||
| + | color TEXT DEFAULT '# | ||
| + | notes TEXT | ||
| + | ); | ||
| + | CREATE TABLE IF NOT EXISTS alarms ( | ||
| + | id | ||
| + | timestamp | ||
| + | session_id | ||
| + | device_id | ||
| + | entity_id | ||
| + | old_state | ||
| + | new_state | ||
| + | value REAL, | ||
| + | acknowledged INTEGER DEFAULT 0, | ||
| + | ack_time | ||
| + | ack_by | ||
| + | ); | ||
| + | CREATE TABLE IF NOT EXISTS exports ( | ||
| + | id | ||
| + | created | ||
| + | session_id TEXT, | ||
| + | format | ||
| + | filename | ||
| + | device_id | ||
| + | entity_id | ||
| + | ); | ||
| + | """ | ||
| + | await self._conn.commit() | ||
| + | | ||
| + | async def ensure_device_table(self, | ||
| + | """ | ||
| + | Erstellt Device-Tabelle wenn sie nicht existiert. | ||
| + | Fügt fehlende Spalten hinzu wenn Device erweitert wurde. | ||
| + | """ | ||
| + | safe_id = device_id.replace(" | ||
| + | table = f" | ||
| + | | ||
| + | base_cols = [ | ||
| + | "id INTEGER PRIMARY KEY AUTOINCREMENT", | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | ] | ||
| + | | ||
| + | entity_cols = [] | ||
| + | for eid, entity in entities.items(): | ||
| + | t = entity.__class__.__name__ | ||
| + | if t == " | ||
| + | entity_cols.append(f" | ||
| + | elif t == " | ||
| + | entity_cols.append(f" | ||
| + | else: | ||
| + | entity_cols.append(f" | ||
| + | | ||
| + | all_cols = base_cols + entity_cols | ||
| + | await self._conn.execute( | ||
| + | f" | ||
| + | ) | ||
| + | await self._conn.execute( | ||
| + | f" | ||
| + | ) | ||
| + | | ||
| + | # Fehlende Spalten ergänzen (Treiber wurde erweitert) | ||
| + | async with self._conn.execute(f" | ||
| + | existing = {row[1] async for row in cur} | ||
| + | | ||
| + | for eid, entity in entities.items(): | ||
| + | if eid not in existing: | ||
| + | col_type = " | ||
| + | await self._conn.execute( | ||
| + | f" | ||
| + | ) | ||
| + | self.logger.info(f" | ||
| + | | ||
| + | await self._conn.commit() | ||
| + | | ||
| + | def queue_snapshot(self, | ||
| + | timestamp: float, values: dict, | ||
| + | alarm_state: | ||
| + | """ | ||
| + | self._batch.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }) | ||
| + | | ||
| + | async def _flush_loop(self): | ||
| + | """ | ||
| + | while True: | ||
| + | await asyncio.sleep(self._flush_interval) | ||
| + | if not self._batch: | ||
| + | continue | ||
| + | batch, self._batch = self._batch, | ||
| + | try: | ||
| + | for snap in batch: | ||
| + | safe_id = snap[" | ||
| + | table = f" | ||
| + | cols = [" | ||
| + | vals = [snap[" | ||
| + | ph = "," | ||
| + | await self._conn.execute( | ||
| + | f" | ||
| + | ) | ||
| + | await self._conn.commit() | ||
| + | except Exception as e: | ||
| + | self.logger.error(f" | ||
| + | | ||
| + | async def query_device(self, | ||
| + | session_id: str = None, | ||
| + | start: float = None, end: float = None, | ||
| + | limit: int = None) -> list: | ||
| + | """ | ||
| + | Liest Messwerte aus einer Device-Tabelle. | ||
| + | entities: Liste der gewünschten Entity-IDs (leere Liste = alle) | ||
| + | """ | ||
| + | safe_id = device_id.replace(" | ||
| + | table = f" | ||
| + | | ||
| + | cols = ", " | ||
| + | where = [] | ||
| + | params = [] | ||
| + | | ||
| + | if session_id: | ||
| + | where.append(" | ||
| + | params.append(session_id) | ||
| + | if start: | ||
| + | where.append(" | ||
| + | params.append(start) | ||
| + | if end: | ||
| + | where.append(" | ||
| + | params.append(end) | ||
| + | | ||
| + | sql = f" | ||
| + | if where: | ||
| + | sql += " WHERE " + " AND " | ||
| + | sql += " ORDER BY timestamp" | ||
| + | if limit: | ||
| + | sql += f" LIMIT {limit}" | ||
| + | | ||
| + | async with self._conn.execute(sql, | ||
| + | rows = await cur.fetchall() | ||
| + | return [dict(r) for r in rows] | ||
| + | | ||
| + | async def add_marker(self, | ||
| + | timestamp: float = None, device_id: str = None, | ||
| + | color: str = "# | ||
| + | ts = timestamp or time.time() | ||
| + | await self._conn.execute( | ||
| + | " | ||
| + | (ts, session_id, device_id, label, color, notes) | ||
| + | ) | ||
| + | await self._conn.commit() | ||
| + | | ||
| + | async def close(self): | ||
| + | if self._batch: | ||
| + | await self._flush_loop() | ||
| + | if self._conn: | ||
| + | await self._conn.close() | ||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Phase 2: ProjectManager Backend ===== | ||
| + | |||
| + | **Warum jetzt?** SQLite steht, jetzt kann die Klammer drumherum gebaut werden. | ||
| + | |||
| + | **Geschätzter Aufwand: 2-3 Tage** | ||
| + | |||
| + | ^ Aufgabe ^ Datei ^ Details ^ | ||
| + | | ProjectManager Klasse | '' | ||
| + | | Engine umbauen | '' | ||
| + | | DeviceManager umbauen | '' | ||
| + | | Migrations-Script | '' | ||
| + | | REST Endpoints | '' | ||
| + | |||
| + | **Wichtig: | ||
| + | |||
| + | **Migration von YAML zu Projekt:** | ||
| + | |||
| + | <code python> | ||
| + | # tools/ | ||
| + | """ | ||
| + | Konvertiert eine bestehende config.yaml in ein ionpy-Projekt. | ||
| + | Aufruf: python -m tools.migrate_config --config config.yaml --output projects/ | ||
| + | """ | ||
| + | |||
| + | import yaml | ||
| + | import json | ||
| + | import os | ||
| + | import uuid | ||
| + | from datetime import datetime | ||
| + | |||
| + | def migrate(config_path: | ||
| + | with open(config_path, | ||
| + | old_config = yaml.safe_load(f) | ||
| + | | ||
| + | os.makedirs(output_path, | ||
| + | os.makedirs(os.path.join(output_path, | ||
| + | os.makedirs(os.path.join(output_path, | ||
| + | | ||
| + | # project.json | ||
| + | project = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | # devices.json — aus YAML-Devices konvertieren | ||
| + | devices = [] | ||
| + | for dev in old_config.get(" | ||
| + | devices.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | if k not in [" | ||
| + | " | ||
| + | }) | ||
| + | | ||
| + | with open(os.path.join(output_path, | ||
| + | json.dump(project, | ||
| + | | ||
| + | with open(os.path.join(output_path, | ||
| + | json.dump({" | ||
| + | | ||
| + | print(f" | ||
| + | print(f" | ||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Phase 3: Minimale Projekt-UI ===== | ||
| + | |||
| + | **Motto: Es muss funktionieren, | ||
| + | |||
| + | **Geschätzter Aufwand: 1 Tag** | ||
| + | |||
| + | Ein Modal beim Start das fragt " | ||
| + | |||
| + | < | ||
| + | ┌─────────────────────────────────────────┐ | ||
| + | │ ionpy Pro OS — Projekt wählen | ||
| + | ├─────────────────────────────────────────┤ | ||
| + | │ ► Akku-Charakterisierung | ||
| + | │ Zuletzt geöffnet: heute 08:12 │ | ||
| + | │ │ | ||
| + | │ ► LED-Treiber Test │ | ||
| + | │ Zuletzt geöffnet: 15.02.2026 | ||
| + | │ │ | ||
| + | │ + Neues Projekt anlegen | ||
| + | └─────────────────────────────────────────┘ | ||
| + | </ | ||
| + | |||
| + | Neue REST Endpoints dafür: | ||
| + | |||
| + | < | ||
| + | GET / | ||
| + | POST / | ||
| + | POST / | ||
| + | GET / | ||
| + | </ | ||
| + | |||
| + | **In dieser Phase:** Devices werden noch per JSON-Editor im Browser hinzugefügt (Übergangslösung). Das ist nicht komfortabel, | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Phase 4: Device-Wizard ===== | ||
| + | |||
| + | **Geschätzter Aufwand: 3-4 Tage** | ||
| + | |||
| + | Das ist der wichtigste UX-Schritt. Der Workflow: | ||
| + | |||
| + | < | ||
| + | Schritt 1: Treiber wählen | ||
| + | → Dropdown aus GET / | ||
| + | → Zeigt Beschreibung aus dem Docstring | ||
| + | → Zeigt welche Transporte unterstützt werden (STACK_BLUEPRINTS) | ||
| + | |||
| + | Schritt 2: Instanz konfigurieren | ||
| + | → ID, Alias, Farbe (immer gleich) | ||
| + | → Transport-Typ wählen (Serial / TCP / ...) | ||
| + | → Wenn Serial: Port-Dropdown aus GET / | ||
| + | → Wenn Serial: port_hint wird automatisch aus VID/PID gesetzt | ||
| + | → Treiber-spezifische params (dynamisch aus driver.parameters) | ||
| + | |||
| + | Schritt 3: Verbindung testen | ||
| + | → " | ||
| + | → Zeigt ersten Messwert wenn erfolgreich | ||
| + | |||
| + | Schritt 4: Speichern | ||
| + | → Landet in devices.json | ||
| + | → Device wird live gespawnt (kein Server-Neustart nötig!) | ||
| + | </ | ||
| + | |||
| + | **" | ||
| + | |||
| + | < | ||
| + | GET / | ||
| + | → User wählt Quell-Projekt | ||
| + | → GET / | ||
| + | → Checkbox-Liste: | ||
| + | → POST / | ||
| + | → Ports müssen ggf. angepasst werden (Dialog) | ||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Phase 5: Quick-Wins (parallel zu jeder Phase machbar) ===== | ||
| + | |||
| + | Diese Aufgaben sind unabhängig und können jederzeit zwischendurch erledigt werden: | ||
| + | |||
| + | ^ Aufgabe ^ Aufwand ^ Datei ^ | ||
| + | | Exponential Backoff beim Reconnect | 1h | '' | ||
| + | | Store.js State als Map (Race-Condition) | 1h | '' | ||
| + | | System-Status Endpoint (/ | ||
| + | | EventBus Topic-Filtering | 2h | '' | ||
| + | | EnumEntity + BitmaskEntity | 3h | '' | ||
| + | |||
| + | **Exponential Backoff (fast fertig, nur einsetzen): | ||
| + | |||
| + | <code python> | ||
| + | async def reconnect(self, | ||
| + | initial_delay: | ||
| + | max_delay: float = 60.0, | ||
| + | backoff_factor: | ||
| + | delay = initial_delay | ||
| + | for attempt in range(1, max_retries + 1): | ||
| + | try: | ||
| + | await self.disconnect() | ||
| + | if self.check_hardware_exists(): | ||
| + | if await self.connect(): | ||
| + | self.logger.info(f" | ||
| + | return True | ||
| + | except Exception as e: | ||
| + | self.logger.debug(f" | ||
| + | | ||
| + | self.logger.info(f" | ||
| + | await asyncio.sleep(delay) | ||
| + | delay = min(delay * backoff_factor, | ||
| + | | ||
| + | return False | ||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Phase 6: AlarmManager ===== | ||
| + | |||
| + | **Warum hier?** Alarm-Berechnung läuft bereits in '' | ||
| + | |||
| + | **Geschätzter Aufwand: 3-4 Tage** | ||
| + | |||
| + | Was hinzukommt: | ||
| + | * '' | ||
| + | * Verknüpfung mit '' | ||
| + | * Alarms in '' | ||
| + | * WebSocket-Event wenn Alarm auftritt (neuer Message-Type) | ||
| + | * Frontend AlarmView: aktive Alarme, History, Quittierung | ||
| + | * Marker in DB setzen wenn Alarm ausgelöst wird (" | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Phase 7: Session-Export ===== | ||
| + | |||
| + | **Geschätzter Aufwand: 2 Tage** | ||
| + | |||
| + | < | ||
| + | GET / | ||
| + | GET / | ||
| + | GET / | ||
| + | </ | ||
| + | |||
| + | Export landet automatisch in '' | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Phase 8: Automation/ | ||
| + | |||
| + | **Achtung: | ||
| + | |||
| + | **Empfohlene Reihenfolge: | ||
| + | - Erst: **Rule-Engine** (YAML-basiert, | ||
| + | - Dann: **Python-Scripting** via exec() — nur für lokale, vertrauenswürdige Umgebung | ||
| + | |||
| + | Scripts gehören in '' | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ====== 5. Prompts für Claude Opus ====== | ||
| + | |||
| + | Diese Prompts bauen aufeinander auf. Immer den relevanten Code als Attachment beifügen. | ||
| + | |||
| + | ===== Prompt 1: ProjectDatabase ===== | ||
| + | |||
| + | < | ||
| + | Ich entwickle " | ||
| + | mit Vue 3 Frontend. | ||
| + | |||
| + | Ich habe mich entschieden ein Projekt-Konzept einzuführen. Ein Projekt ist | ||
| + | ein Ordner der alles enthält: devices.json (Gerätedefinitionen), | ||
| + | (UI-Layout) und data.db (SQLite für alle Messdaten). | ||
| + | |||
| + | Ich hänge dir an: | ||
| + | - core/ | ||
| + | - core/ | ||
| + | - structures/ | ||
| + | |||
| + | Bitte implementiere die Klasse ProjectDatabase in core/ | ||
| + | mit folgenden Anforderungen: | ||
| + | |||
| + | 1. Eine SQLite-Datei pro Projekt (data.db im Projekt-Ordner) | ||
| + | 2. Eine Tabelle pro Device - Spalten sind die Entity-IDs des Treibers | ||
| + | 3. Methode ensure_device_table(device_id, | ||
| + | | ||
| + | 4. Batch-Insert via asyncio: queue_snapshot() ist nicht-blockierend, | ||
| + | | ||
| + | | ||
| + | 5. WAL-Mode und PRAGMA synchronous=NORMAL für Performance | ||
| + | 6. Basis-Tabellen: | ||
| + | 7. Methode query_device() mit optionalem Filter auf session_id, Zeitbereich, | ||
| + | | ||
| + | 8. Methode add_marker() für Chart-Annotationen | ||
| + | |||
| + | Das Schema für die Basis-Tabellen: | ||
| + | |||
| + | Bitte keinen neuen Connection-Handler pro Query - eine persistente Connection | ||
| + | für die gesamte Laufzeit. | ||
| + | </ | ||
| + | |||
| + | ===== Prompt 2: ProjectManager ===== | ||
| + | |||
| + | < | ||
| + | Ich habe jetzt die ProjectDatabase (core/ | ||
| + | |||
| + | Bitte implementiere den ProjectManager in core/ | ||
| + | |||
| + | Er verwaltet: | ||
| + | - projects/ Ordner-Struktur (ein Unterordner pro Projekt) | ||
| + | - project.json (Metadaten, schema_version, | ||
| + | - devices.json (Gerätedefinitionen, | ||
| + | - Schnittstelle zur ProjectDatabase | ||
| + | |||
| + | Das Format von project.json und devices.json: | ||
| + | [JSONs hier einfügen] | ||
| + | |||
| + | Wichtige Methoden: | ||
| + | - list_projects() → alle Projekte im projects/ Ordner | ||
| + | - create_project(name, | ||
| + | - open_project(project_id) → lädt project.json und devices.json | ||
| + | - add_device(device_dict) → fügt Device zu devices.json hinzu und | ||
| + | validiert Pflichtfelder (id, driver, transport.type) | ||
| + | - remove_device(device_id) → entfernt aus devices.json | ||
| + | - start_session(name, | ||
| + | setzt active_session in project.json | ||
| + | - stop_session() → setzt stopped-Timestamp und status=' | ||
| + | - get_config_for_engine() → gibt das Dict zurück das SystemEngine | ||
| + | bisher als config_path erwartet hat (für Rückwärtskompatibilität) | ||
| + | |||
| + | Bestehender Code den du NICHT verändern sollst: | ||
| + | - device_manager._spawn_device() bekommt weiterhin ein Dict | ||
| + | - Alles in structures/ bleibt unverändert | ||
| + | - Die YAML-basierte Config soll noch als Fallback funktionieren | ||
| + | </ | ||
| + | |||
| + | ===== Prompt 3: Engine umbauen ===== | ||
| + | |||
| + | < | ||
| + | Ich habe jetzt ProjectManager und ProjectDatabase implementiert. | ||
| + | Ich hänge dir an: core/ | ||
| + | |||
| + | Bitte passe die SystemEngine in core/ | ||
| + | |||
| + | 1. __init__ nimmt jetzt optional einen project: ProjectManager Parameter | ||
| + | - Wenn project übergeben: devices aus project.devices_path laden | ||
| + | - Wenn nicht (Legacy): weiterhin config_path verwenden | ||
| + | |||
| + | 2. Einen _storage_worker Task hinzufügen der parallel zum _cache_worker läuft: | ||
| + | - Lauscht auf den EventBus | ||
| + | - Ruft project.db.queue_snapshot() auf für jeden Sample | ||
| + | - Filtert WaveformSamples heraus (die werden nicht in die DB geschrieben) | ||
| + | - Nur aktiv wenn eine Session läuft (session_manager.is_recording) | ||
| + | |||
| + | 3. Bei engine.start(): | ||
| + | | ||
| + | |||
| + | 4. engine.stop(): | ||
| + | |||
| + | Wichtig: Der bestehende _cache_worker und der EventBus bleiben unverändert. | ||
| + | Der Storage Worker ist ein zusätzlicher Subscriber. | ||
| + | </ | ||
| + | |||
| + | ===== Prompt 4: REST Endpoints für Projekt-Management ===== | ||
| + | |||
| + | < | ||
| + | Ich habe jetzt ProjectManager, | ||
| + | Angehängt: web/rest.py (aktuell), core/ | ||
| + | |||
| + | Bitte füge folgende REST Endpoints zu web/rest.py hinzu: | ||
| + | |||
| + | Projekt-Management: | ||
| + | GET / | ||
| + | POST / | ||
| + | GET / | ||
| + | POST / | ||
| + | |||
| + | Device-Management im aktiven Projekt: | ||
| + | GET / | ||
| + | POST / | ||
| + | DELETE / | ||
| + | |||
| + | Session-Management: | ||
| + | GET / | ||
| + | POST / | ||
| + | POST / | ||
| + | |||
| + | Marker: | ||
| + | POST / | ||
| + | |||
| + | History (für Chart-Daten): | ||
| + | GET / | ||
| + | Query-Parameter: | ||
| + | |||
| + | Fehlerbehandlung: | ||
| + | - 404 wenn Projekt nicht gefunden | ||
| + | - 409 wenn Device-ID bereits existiert | ||
| + | - 400 wenn Pflichtfelder fehlen | ||
| + | - 500 mit aussagekräftiger Fehlermeldung bei DB-Fehlern | ||
| + | |||
| + | Alle Endpoints die das aktive Projekt benötigen sollen eine | ||
| + | HTTPException(503, | ||
| + | </ | ||
| + | |||
| + | ===== Prompt 5: Migration und CLI-Tool ===== | ||
| + | |||
| + | < | ||
| + | Bitte erstelle tools/ | ||
| + | |||
| + | Das Script konvertiert eine bestehende ionpy config.yaml in die neue | ||
| + | Projekt-Struktur. | ||
| + | |||
| + | Aufruf: python -m tools.migrate_config --config config.yaml --name "Mein Projekt" | ||
| + | |||
| + | Was es tun soll: | ||
| + | 1. config.yaml einlesen | ||
| + | 2. Projekt-Ordner anlegen (projects/ | ||
| + | 3. project.json generieren (mit neuer UUID, Datum, name) | ||
| + | 4. devices.json generieren: | ||
| + | - Jeder YAML-Device wird zu einem JSON-Device | ||
| + | - transport-Block bleibt wie er ist | ||
| + | - Alle unbekannten Felder landen in params{} | ||
| + | - port_hint wird als leeres Objekt angelegt (muss manuell befüllt werden) | ||
| + | 5. Hinweis ausgeben: "Bitte workspace.json manuell kopieren" | ||
| + | 6. Validierung: | ||
| + | |||
| + | Das Format von devices.json und project.json ist hier definiert: | ||
| + | [JSONs einfügen] | ||
| + | </ | ||
| + | |||
| + | ===== Prompt 6: Device-Wizard Frontend ===== | ||
| + | |||
| + | < | ||
| + | Ich habe jetzt das Backend für das Projekt-Management vollständig. | ||
| + | Angehängt: static/ | ||
| + | |||
| + | Bitte implementiere den Device-Wizard als neues Vue 3 Widget. | ||
| + | Er soll als Modal über dem Dashboard erscheinen. | ||
| + | |||
| + | Schritt 1 — Treiber wählen: | ||
| + | - Lädt GET / | ||
| + | - Zeigt Liste mit Treibername und Beschreibung (aus driver.description) | ||
| + | - Zeigt welche Transport-Typen der Treiber unterstützt (aus STACK_BLUEPRINTS) | ||
| + | |||
| + | Schritt 2 — Instanz konfigurieren: | ||
| + | - Pflichtfelder: | ||
| + | - Transport-Typ Dropdown (die Keys aus STACK_BLUEPRINTS des gewählten Treibers) | ||
| + | - Wenn transport.type == " | ||
| + | Lädt GET / | ||
| + | Zeigt Port-Dropdown mit Beschreibungen | ||
| + | - Wenn transport.type == " | ||
| + | - Treiber-spezifische params aus driver.parameters dynamisch als Formular-Felder | ||
| + | |||
| + | Schritt 3 — Test: | ||
| + | - " | ||
| + | - POST / | ||
| + | - Zeigt Erfolg/ | ||
| + | |||
| + | Schritt 4 — Speichern: | ||
| + | - POST / | ||
| + | - Device erscheint sofort im Geräte-Menü | ||
| + | |||
| + | Style: Exakt gleicher Dark-Theme Stil wie der Rest der App | ||
| + | (Farben aus den CSS-Variablen). Kein zusätzliches CSS-Framework. | ||
| + | |||
| + | Wichtig: Kein < | ||
| + | </ | ||
| + | |||
| + | ===== Prompt 7: Projekt-Auswahl beim Start ===== | ||
| + | |||
| + | < | ||
| + | Bitte füge dem Frontend (index.html) einen Projekt-Auswahl Dialog hinzu. | ||
| + | |||
| + | Er soll beim ersten Laden erscheinen wenn kein Projekt aktiv ist | ||
| + | (GET / | ||
| + | |||
| + | Das Modal zeigt: | ||
| + | - Liste aller Projekte (GET / | ||
| + | Pro Eintrag: Name, Beschreibung, | ||
| + | Klick → POST / | ||
| + | - Button "Neues Projekt anlegen" | ||
| + | → Formular: Name (Pflicht), Beschreibung (optional) | ||
| + | → POST / | ||
| + | |||
| + | Das Modal kann NICHT geschlossen werden ohne ein Projekt zu wählen. | ||
| + | Es ist kein Widget - es ist ein Fullscreen-Overlay das den Rest der App blockiert. | ||
| + | |||
| + | Wenn ein Projekt aktiv ist: Projekt-Name im Header anzeigen | ||
| + | (neben dem "ionpy PRO" Schriftzug). | ||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ====== 6. Offene Fragen & Entscheidungen ====== | ||
| + | |||
| + | Diese Punkte wurden in der Konzept-Session bewusst offen gelassen: | ||
| + | |||
| + | ^ Frage ^ Empfehlung ^ Priorität ^ | ||
| + | | Port-Portabilität: | ||
| + | | Treiber-Versionierung: | ||
| + | | Downsampling: | ||
| + | | Automation: exec() oder Rule-Engine zuerst? | Rule-Engine (YAML-basiert) zuerst, sicherer | Phase 8 | | ||
| + | | Waveform-Daten in DB? | Nein, Waveforms in eigene Dateien (zu groß für SQLite-Rows) | Offen | | ||
| + | | Multi-User / Concurrent Access? | Aktuell kein Ziel, WAL-Mode gibt aber schon etwas Sicherheit | Nicht geplant | | ||
| + | |||
| + | ---- | ||
| + | |||
| + | //Erstellt im Rahmen der ionpy Architektur-Session, | ||
| + | //Nächster Schritt: Implementierung mit Claude Opus, beginnend mit Phase 1 (ProjectDatabase)// | ||
ionpy/project_rodemap.txt · Last modified: by dominik
