Table of Contents
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/FastAPI Backend mit Vue 3 Frontend zur Steuerung und Visualisierung von Laborgeräten. Die Architektur folgt dem Muster:
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/Framing Stack mit Factory & Registry
- 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 ← Metadaten, Klammer
├── devices.json ← Welche Geräte, wie angeschlossen
├── workspace.json ← UI-Layout (bestehendes Format)
├── data.db ← EINE SQLite DB für das gesamte Projekt
├── scripts/ ← Automations-Scripte
│ └── charge_test.py
└── exports/ ← Alle Exports, egal welche Session
└── PSU_01_session1.csv
2.2 Die JSON-Strukturen
project.json
{
"schema_version": 1,
"id": "a3f8c2d1-4b5e-4f6a-9c2d-1e7f8b3a2c5d",
"name": "Akku-Charakterisierung 18650",
"description": "Kapazitätsmessung nach Lagerung bei verschiedenen Temperaturen",
"created": "2026-01-15T09:23:00",
"last_opened": "2026-02-28T08:12:00",
"author": "Marcus",
"tags": ["battery", "18650", "characterization"],
"active_session": "b7e2a1f4-3c8d-4e9b-a2f5-6d1c8e4b3a7f",
"files": {
"devices": "devices.json",
"workspace": "workspace.json"
},
"notes": "Zellen wurden 30 Tage bei 10°C gelagert."
}
Warum schema_version? Spätere Format-Änderungen können automatisch migriert werden.
Warum UUID als id? “Copy devices from Project XY” referenziert über ID, nicht über Namen.
Warum files-Block? Flexibilität — theoretisch kann jedes Projekt eine andere Workspace-Datei haben.
devices.json
{
"schema_version": 1,
"devices": [
{
"id": "PSU_01",
"driver": "rd6012",
"enabled": true,
"alias": "Lade-PSU",
"color": "#0ea5e9",
"icon": "power-plug",
"location": "Rack links, Shelf 2",
"auto_start": true,
"transport": {
"type": "serial",
"port": "COM3",
"baudrate": 115200,
"port_hint": {
"vid": "0x1A86",
"pid": "0x7523",
"description": "USB-Serial CH340"
}
},
"polling_interval_ms": 500,
"params": {
"modbus_address": 1
},
"logging": {
"override_level": null
}
}
]
}
Wichtige Entscheidungen in dieser Struktur:
idist intern und unveränderlich (wird in DB referenziert)aliasist was der User sieht und ändern darfcolorundicongehören zur Device-Definition, nicht zum Workspaceport_hintmit VID/PID ist die Lösung für Portabilität auf anderen Rechnernparamsist ein freies Objekt für treiberspezifische Konfiguration (Modbus-Adresse etc.)
2.3 Das Datenbank-Schema
Entscheidung: Eine DB pro Projekt, eine Tabelle pro Device.
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/ergänzt
-- Wird beim ersten Start des Projekts angelegt CREATE TABLE sessions ( id TEXT PRIMARY KEY, name TEXT NOT NULL, created REAL NOT NULL, stopped REAL, STATUS TEXT DEFAULT 'running', conditions TEXT, -- JSON blob: Temperatur, Luftfeuchte, Notizen notes TEXT ); -- Zeitmarker für Chart-Annotation und Daten-Segmentierung CREATE TABLE markers ( id INTEGER PRIMARY KEY AUTOINCREMENT, TIMESTAMP REAL NOT NULL, session_id TEXT NOT NULL, device_id TEXT, -- NULL = gilt für alle Devices label TEXT NOT NULL, -- z.B. "Ladung Start", "Entladung Start" color TEXT DEFAULT '#f59e0b', notes TEXT ); -- Alarm-Verlauf CREATE TABLE alarms ( id INTEGER PRIMARY KEY AUTOINCREMENT, TIMESTAMP REAL NOT NULL, session_id TEXT NOT NULL, device_id TEXT NOT NULL, entity_id TEXT NOT NULL, old_state TEXT, new_state TEXT, VALUE REAL, acknowledged INTEGER DEFAULT 0, ack_time REAL, ack_by TEXT ); -- Export-Historie CREATE TABLE exports ( id INTEGER PRIMARY KEY AUTOINCREMENT, created REAL NOT NULL, session_id TEXT, -- NULL = ganzes Projekt format TEXT, filename TEXT, device_id TEXT, entity_id TEXT ); -- Device-Tabellen werden DYNAMISCH angelegt: -- CREATE TABLE device_PSU_01 ( -- id INTEGER PRIMARY KEY AUTOINCREMENT, -- timestamp REAL NOT NULL, -- session_id TEXT NOT NULL, -- u_out REAL, -- i_out REAL, -- p_out REAL, -- output INTEGER, -- alarm_state TEXT DEFAULT 'NORMAL' -- ); -- CREATE INDEX idx_PSU_01 ON device_PSU_01(session_id, timestamp);
Abfrage-Beispiele:
-- Chart: 100 Spannungswerte einer Session SELECT TIMESTAMP, u_out, i_out FROM device_PSU_01 WHERE session_id = 'abc123' AND TIMESTAMP BETWEEN 1706000000 AND 1706003600 ORDER BY TIMESTAMP; -- Session-Vergleich: Max-Spannung pro Session 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, d.u_out, d.i_out FROM device_PSU_01 d WHERE d.session_id = 'abc123' AND d.timestamp BETWEEN (SELECT TIMESTAMP FROM markers WHERE label='Ladung Start' AND session_id='abc123') AND (SELECT TIMESTAMP FROM markers WHERE label='Ladung Ende' AND session_id='abc123');
3. Gesamtarchitektur (Zielzustand)
┌─────────────────────────────────────────────────────────────┐ │ Vue 3 Frontend │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ │ │ Projekt- │ │ Device- │ │Dashboard │ │ AlarmView / │ │ │ │ Manager │ │ Wizard │ │(wie jetzt│ │ HistoryView │ │ │ └──────────┘ └──────────┘ └──────────┘ └───────────────┘ │ │ globalStore (reactive) │ │ │ WebSocket │ ├─────────────────────────┼───────────────────────────────────┤ │ FastAPI Server │ │ ┌──────────────┐ ┌────────────┐ ┌─────────────────────┐ │ │ │ REST API │ │ WS Route │ │ Static Files │ │ │ │ /api/projects│ │ │ │ │ │ │ └──────┬───────┘ └─────┬──────┘ └─────────────────────┘ │ │ └────────────────┘ │ │ SystemEngine │ │ ┌──────────────┬───────────────┬────────────────┐ │ │ ProjectManager DeviceManager AlarmManager │ │ │ │ │ │ │ │ │ project.json devices.json EventBus │ │ │ sessions Treiber-Loading (Topic-Filter) │ │ │ │ │ │ │ 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 data.db in den Projekt-Ordner gehört.
Geschätzter Aufwand: 2 Tage
| Aufgabe | Datei | Details |
|---|---|---|
| ProjectDatabase Klasse | core/project_database.py | Persistente Connection, WAL-Mode, Batch-Insert |
| ensure_device_table() | core/project_database.py | Dynamisches Schema, ALTER TABLE für neue Entities |
| Storage Worker in Engine | core/engine.py | Parallel zum Cache-Worker, Batch alle 500ms |
| Marker-API | core/project_database.py | add_marker(), get_markers() |
| History REST Endpoint | web/rest.py | GET /api/devices/{id}/history mit Zeitbereich |
Kern-Klasse:
# core/project_database.py 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, project_path: str): self.db_path = os.path.join(project_path, "data.db") self._conn: Optional[aiosqlite.Connection] = None self._batch: list = [] self._flush_interval = 0.5 self.logger = logging.getLogger("ProjectDatabase") async def initialize(self): self._conn = await aiosqlite.connect(self.db_path) self._conn.row_factory = aiosqlite.Row # WAL-Mode: bessere Concurrent-Performance, kein Full-Lock beim Schreiben await self._conn.execute("PRAGMA journal_mode=WAL") await self._conn.execute("PRAGMA synchronous=NORMAL") await self._create_base_schema() asyncio.create_task(self._flush_loop()) self.logger.info(f"Datenbank geöffnet: {self.db_path}") 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 REAL NOT NULL, stopped REAL, status TEXT DEFAULT 'running', conditions TEXT, notes TEXT ); CREATE TABLE IF NOT EXISTS markers ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL NOT NULL, session_id TEXT NOT NULL, device_id TEXT, label TEXT NOT NULL, color TEXT DEFAULT '#f59e0b', notes TEXT ); CREATE TABLE IF NOT EXISTS alarms ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL NOT NULL, session_id TEXT NOT NULL, device_id TEXT NOT NULL, entity_id TEXT NOT NULL, old_state TEXT, new_state TEXT, value REAL, acknowledged INTEGER DEFAULT 0, ack_time REAL, ack_by TEXT ); CREATE TABLE IF NOT EXISTS exports ( id INTEGER PRIMARY KEY AUTOINCREMENT, created REAL NOT NULL, session_id TEXT, format TEXT, filename TEXT, device_id TEXT, entity_id TEXT ); """) await self._conn.commit() async def ensure_device_table(self, device_id: str, entities: dict): """ Erstellt Device-Tabelle wenn sie nicht existiert. Fügt fehlende Spalten hinzu wenn Device erweitert wurde. """ safe_id = device_id.replace("-", "_").replace(".", "_") table = f"device_{safe_id}" base_cols = [ "id INTEGER PRIMARY KEY AUTOINCREMENT", "timestamp REAL NOT NULL", "session_id TEXT NOT NULL", "alarm_state TEXT DEFAULT 'NORMAL'" ] entity_cols = [] for eid, entity in entities.items(): t = entity.__class__.__name__ if t == "NumericEntity": entity_cols.append(f"{eid} REAL") elif t == "ToggleEntity": entity_cols.append(f"{eid} INTEGER") else: entity_cols.append(f"{eid} TEXT") all_cols = base_cols + entity_cols await self._conn.execute( f"CREATE TABLE IF NOT EXISTS {table} ({', '.join(all_cols)})" ) await self._conn.execute( f"CREATE INDEX IF NOT EXISTS idx_{safe_id} ON {table}(session_id, timestamp)" ) # Fehlende Spalten ergänzen (Treiber wurde erweitert) async with self._conn.execute(f"PRAGMA table_info({table})") as cur: existing = {row[1] async for row in cur} for eid, entity in entities.items(): if eid not in existing: col_type = "REAL" if entity.__class__.__name__ == "NumericEntity" else "TEXT" await self._conn.execute( f"ALTER TABLE {table} ADD COLUMN {eid} {col_type}" ) self.logger.info(f"Neue Spalte: {table}.{eid}") await self._conn.commit() def queue_snapshot(self, device_id: str, session_id: str, timestamp: float, values: dict, alarm_state: str = "NORMAL"): """Fügt Snapshot zur Batch-Queue hinzu. Nicht-blockierend.""" self._batch.append({ "device_id": device_id, "session_id": session_id, "timestamp": timestamp, "values": values, "alarm_state": alarm_state }) async def _flush_loop(self): """Schreibt alle 500ms gesammelte Snapshots in einem Batch.""" 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["device_id"].replace("-","_").replace(".","_") table = f"device_{safe_id}" cols = ["timestamp","session_id","alarm_state"] + list(snap["values"].keys()) vals = [snap["timestamp"], snap["session_id"], snap["alarm_state"]] + list(snap["values"].values()) ph = ",".join(["?"]*len(cols)) await self._conn.execute( f"INSERT INTO {table} ({','.join(cols)}) VALUES ({ph})", vals ) await self._conn.commit() except Exception as e: self.logger.error(f"Flush-Fehler: {e}") async def query_device(self, device_id: str, entities: list, 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("-","_").replace(".","_") table = f"device_{safe_id}" cols = ", ".join(["timestamp"] + entities) if entities else "*" where = [] params = [] if session_id: where.append("session_id = ?") params.append(session_id) if start: where.append("timestamp >= ?") params.append(start) if end: where.append("timestamp <= ?") params.append(end) sql = f"SELECT {cols} FROM {table}" if where: sql += " WHERE " + " AND ".join(where) sql += " ORDER BY timestamp" if limit: sql += f" LIMIT {limit}" async with self._conn.execute(sql, params) as cur: rows = await cur.fetchall() return [dict(r) for r in rows] async def add_marker(self, session_id: str, label: str, timestamp: float = None, device_id: str = None, color: str = "#f59e0b", notes: str = None): ts = timestamp or time.time() await self._conn.execute( "INSERT INTO markers (timestamp, session_id, device_id, label, color, notes) VALUES (?,?,?,?,?,?)", (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 | core/project_manager.py | Ordner, JSONs, Sessions verwalten |
| Engine umbauen | core/engine.py | Nimmt Project statt config_path |
| DeviceManager umbauen | core/device_manager.py | Liest devices.json statt YAML |
| Migrations-Script | tools/migrate_config.py | YAML → Projekt-Struktur konvertieren |
| REST Endpoints | web/rest.py | Projekt CRUD, Session Start/Stop |
Wichtig: Der bestehende _spawn_device() bleibt unverändert — er bekommt weiterhin ein Dict. Nur die Quelle ändert sich von YAML zu JSON.
Migration von YAML zu Projekt:
# tools/migrate_config.py """ Konvertiert eine bestehende config.yaml in ein ionpy-Projekt. Aufruf: python -m tools.migrate_config --config config.yaml --output projects/mein-projekt """ import yaml import json import os import uuid from datetime import datetime def migrate(config_path: str, output_path: str, project_name: str): with open(config_path, 'r') as f: old_config = yaml.safe_load(f) os.makedirs(output_path, exist_ok=True) os.makedirs(os.path.join(output_path, "scripts"), exist_ok=True) os.makedirs(os.path.join(output_path, "exports"), exist_ok=True) # project.json project = { "schema_version": 1, "id": str(uuid.uuid4()), "name": project_name, "created": datetime.now().isoformat(), "last_opened": datetime.now().isoformat(), "files": { "devices": "devices.json", "workspace": "workspace.json" } } # devices.json — aus YAML-Devices konvertieren devices = [] for dev in old_config.get("devices", []): devices.append({ "id": dev.get("id"), "driver": dev.get("driver"), "enabled": dev.get("enabled", True), "alias": dev.get("alias", dev.get("id")), "color": dev.get("color", "#0ea5e9"), "auto_start": dev.get("auto_start", True), "transport": dev.get("transport", {}), "polling_interval_ms": dev.get("polling_interval_ms", 1000), "params": {k: v for k, v in dev.items() if k not in ["id","driver","enabled","alias","color", "auto_start","transport","polling_interval_ms"]} }) with open(os.path.join(output_path, "project.json"), 'w') as f: json.dump(project, f, indent=2) with open(os.path.join(output_path, "devices.json"), 'w') as f: json.dump({"schema_version": 1, "devices": devices}, f, indent=2) print(f"Projekt angelegt: {output_path}") print(f"Bitte workspace.json manuell in {output_path} kopieren.")
Phase 3: Minimale Projekt-UI
Motto: Es muss funktionieren, nicht schön sein.
Geschätzter Aufwand: 1 Tag
Ein Modal beim Start das fragt “Welches Projekt?”:
┌─────────────────────────────────────────┐ │ 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 /api/projects → alle Projekte auflisten
POST /api/projects → neues Projekt anlegen
POST /api/projects/{id}/open → Projekt aktivieren (Engine reload)
GET /api/projects/active → aktuell geladenes Projekt
In dieser Phase: Devices werden noch per JSON-Editor im Browser hinzugefügt (Übergangslösung). Das ist nicht komfortabel, aber es entblockt die weitere Entwicklung.
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 /api/config/drivers (bereits vorhanden!) → 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 /api/inventory/tools/serial → Wenn Serial: port_hint wird automatisch aus VID/PID gesetzt → Treiber-spezifische params (dynamisch aus driver.parameters) Schritt 3: Verbindung testen → "Verbinden" Button → sofortiges Feedback → Zeigt ersten Messwert wenn erfolgreich Schritt 4: Speichern → Landet in devices.json → Device wird live gespawnt (kein Server-Neustart nötig!)
“Copy devices from Project XY”:
GET /api/projects → Liste aller Projekte
→ User wählt Quell-Projekt
→ GET /api/projects/{id}/devices → devices.json des Quell-Projekts
→ Checkbox-Liste: welche Devices übernehmen?
→ POST /api/projects/active/devices/import
→ 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 | devices/transport/base.py |
| Store.js State als Map (Race-Condition) | 1h | static/js/store.js |
| System-Status Endpoint (/api/system/status) | 2h | web/rest.py |
| EventBus Topic-Filtering | 2h | core/event_bus.py |
| EnumEntity + BitmaskEntity | 3h | structures/entities/logic.py |
Exponential Backoff (fast fertig, nur einsetzen):
async def reconnect(self, max_retries: int = 10, initial_delay: float = 2.0, max_delay: float = 60.0, backoff_factor: float = 1.5) -> bool: 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"Wiederverbunden nach {attempt} Versuch(en)") return True except Exception as e: self.logger.debug(f"Reconnect #{attempt} Fehler: {e}") self.logger.info(f"Versuch {attempt} fehlgeschlagen. Nächster in {delay:.1f}s") await asyncio.sleep(delay) delay = min(delay * backoff_factor, max_delay) return False
Phase 6: AlarmManager
Warum hier? Alarm-Berechnung läuft bereits in _evaluate_alarms(). Die Hälfte ist schon da.
Geschätzter Aufwand: 3-4 Tage
Was hinzukommt:
core/alarm_manager.py— AlarmEvent, Active-Alarms, History, Callbacks- Verknüpfung mit
_evaluate_alarms()in den Entity-Klassen - Alarms in
ProjectDatabase.alarmsspeichern - WebSocket-Event wenn Alarm auftritt (neuer Message-Type)
- Frontend AlarmView: aktive Alarme, History, Quittierung
- Marker in DB setzen wenn Alarm ausgelöst wird (“ALARM: OVP PSU_01”)
Phase 7: Session-Export
Geschätzter Aufwand: 2 Tage
GET /api/projects/active/sessions/{id}/export?format=csv&device=PSU_01
GET /api/projects/active/sessions/{id}/export?format=json
GET /api/projects/active/export?format=csv ← ganzes Projekt
Export landet automatisch in projects/xy/exports/ und wird in der exports Tabelle geloggt.
Phase 8: Automation/Scripting
Achtung: Den exec()-basierten Ansatz aus der Roadmap-Analyse mit Vorsicht angehen.
Empfohlene Reihenfolge:
- Erst: Rule-Engine (YAML-basiert, Condition → Action) — sicher, einfach
- Dann: Python-Scripting via exec() — nur für lokale, vertrauenswürdige Umgebung
Scripts gehören in projects/xy/scripts/ und sind Teil des Projekts.
5. Prompts für Claude Opus
Diese Prompts bauen aufeinander auf. Immer den relevanten Code als Attachment beifügen.
Prompt 1: ProjectDatabase
Ich entwickle "ionpy" - ein Python/FastAPI Backend zur Laborgerätesteuerung 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), workspace.json (UI-Layout) und data.db (SQLite für alle Messdaten). Ich hänge dir an: - core/engine.py (die SystemEngine) - core/device_manager.py - structures/entities/ (das Entity-ORM) Bitte implementiere die Klasse ProjectDatabase in core/project_database.py 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, entities_dict) die die Tabelle dynamisch anlegt und bei neuen Entities ALTER TABLE ADD COLUMN ausführt 4. Batch-Insert via asyncio: queue_snapshot() ist nicht-blockierend, ein _flush_loop() Task schreibt alle 500ms gesammelte Snapshots in einem executemany()-Aufruf 5. WAL-Mode und PRAGMA synchronous=NORMAL für Performance 6. Basis-Tabellen: sessions, markers, alarms, exports (Schema siehe unten) 7. Methode query_device() mit optionalem Filter auf session_id, Zeitbereich, entity-Liste und LIMIT 8. Methode add_marker() für Chart-Annotationen Das Schema für die Basis-Tabellen: [sessions/markers/alarms/exports Schema hier einfügen] Bitte keinen neuen Connection-Handler pro Query - eine persistente Connection für die gesamte Laufzeit.
Prompt 2: ProjectManager
Ich habe jetzt die ProjectDatabase (core/project_database.py - angehängt). Bitte implementiere den ProjectManager in core/project_manager.py. Er verwaltet: - projects/ Ordner-Struktur (ein Unterordner pro Projekt) - project.json (Metadaten, schema_version, UUID, name, active_session) - devices.json (Gerätedefinitionen, ersetzt die bisherige config.yaml) - 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, description) → legt Ordner und JSONs an - 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, conditions=None) → legt Session in DB an, setzt active_session in project.json - stop_session() → setzt stopped-Timestamp und status='completed' - 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/engine.py (aktueller Stand), core/project_manager.py Bitte passe die SystemEngine in core/engine.py an: 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(): ensure_device_table() für jedes Device aufrufen nachdem die Devices geladen wurden 4. engine.stop(): project.db.close() aufrufen 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, ProjectDatabase und die angepasste Engine.
Angehängt: web/rest.py (aktuell), core/project_manager.py
Bitte füge folgende REST Endpoints zu web/rest.py hinzu:
Projekt-Management:
GET /api/projects → list_projects()
POST /api/projects → create_project(name, description)
GET /api/projects/active → aktuell geladenes Projekt
POST /api/projects/{id}/open → Projekt wechseln (Engine neu initialisieren)
Device-Management im aktiven Projekt:
GET /api/projects/active/devices → devices.json
POST /api/projects/active/devices → add_device()
DELETE /api/projects/active/devices/{id} → remove_device()
Session-Management:
GET /api/projects/active/sessions → alle Sessions aus DB
POST /api/projects/active/sessions/start → start_session(name, conditions)
POST /api/projects/active/sessions/stop → stop_session()
Marker:
POST /api/projects/active/markers → add_marker(label, timestamp?, device_id?, color?)
History (für Chart-Daten):
GET /api/projects/active/devices/{device_id}/history
Query-Parameter: session_id, start (Unix-Timestamp), end, entities (kommagetrennt), limit
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, "Kein Projekt geladen") werfen wenn kein Projekt aktiv ist.
Prompt 5: Migration und CLI-Tool
Bitte erstelle tools/migrate_config.py
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" --output projects/
Was es tun soll:
1. config.yaml einlesen
2. Projekt-Ordner anlegen (projects/{slug-des-namens}/)
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: Warnung wenn ein Device kein 'driver' Feld hat
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/index.html (komplett), die aktuellen REST Endpoints
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 /api/config/drivers
- 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: ID (Text, wird zu device_id), Alias, Farbe (Color-Picker)
- Transport-Typ Dropdown (die Keys aus STACK_BLUEPRINTS des gewählten Treibers)
- Wenn transport.type == "serial":
Lädt GET /api/inventory/tools/serial
Zeigt Port-Dropdown mit Beschreibungen
- Wenn transport.type == "tcp": Host und Port Felder
- Treiber-spezifische params aus driver.parameters dynamisch als Formular-Felder
Schritt 3 — Test:
- "Verbindung testen" Button
- POST /api/projects/active/devices mit {test: true} Flag
- Zeigt Erfolg/Fehler und wenn erfolgreich den ersten Messwert
Schritt 4 — Speichern:
- POST /api/projects/active/devices (ohne test Flag)
- 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 <form> Tag verwenden, nur @click/@input Handler.
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 /api/projects/active gibt 503 zurück).
Das Modal zeigt:
- Liste aller Projekte (GET /api/projects)
Pro Eintrag: Name, Beschreibung, "Zuletzt geöffnet" Datum
Klick → POST /api/projects/{id}/open → Dashboard lädt
- Button "Neues Projekt anlegen"
→ Formular: Name (Pflicht), Beschreibung (optional)
→ POST /api/projects → öffnet das neue Projekt direkt
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: Was wenn COM3 auf neuem Rechner nicht existiert? | VID/PID in port_hint implementieren, Server sucht automatisch | Phase 4 |
| Treiber-Versionierung: Was wenn Treiber sich ändert und alte Session-Daten nicht mehr passen? | driver_version in devices_snapshot der Session speichern | Phase 2 |
| Downsampling: Wann werden historische Daten zu groß? | Retention-Policy als Hintergrund-Task, erst wenn nötig | Später |
| 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, 2026-02-28
Nächster Schritt: Implementierung mit Claude Opus, beginnend mit Phase 1 (ProjectDatabase)
