====== 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:**
* ''id'' ist intern und unveränderlich (wird in DB referenziert)
* ''alias'' ist was der User sieht und ändern darf
* ''color'' und ''icon'' gehören zur Device-Definition, nicht zum Workspace
* ''port_hint'' mit VID/PID ist die Lösung für Portabilität auf anderen Rechnern
* ''params'' ist 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.alarms'' speichern
* 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
===== 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)//