ICM FeliX · Laborautomatisierung · gRPC/TLS

Ein eigener SiLA2-Client — Schritt für Schritt, in Python

Vom leeren Verzeichnis zum laufenden Client, der ein komplettes Pipettier-Protokoll auf dem FeliX ausführt — inklusive Bedienhinweisen, die euer Client anzeigt und quittiert. Modul 09 führt Python direkt im Browser aus — gegen ein echtes FeliX.

10 Module ~90 Minuten Endergebnis felix_client.py mit Python-Sandbox verifiziert 2026-08-10
MODUL 00

SiLA2 in fünf Minuten

SiLA 2 ist der Laborautomatisierungs-Standard für Gerätesteuerung über gRPC + TLS. Die Begriffe, die ihr braucht:

BegriffBedeutung
SiLA ServerDas Gerät bzw. seine Steuersoftware — hier das ICM FeliX (ICM.FeliX.Host)
FeatureEine benannte Fähigkeit, beschrieben als XML (FDL). Der FeliX hat zwei: das Pflicht-Feature SiLAService und com.endress/liquidhandling/LiquidHandlingController/v1
Observable CommandLang laufende Aktion: sofortige CommandExecutionUUID, Fortschritt als Stream, Ergebnis separat
Intermediate ResponseZwischenmeldung eines laufenden Commands — der FeliX nutzt sie für Bedienhinweise
Untrusted-Server-ModusTLS mit selbstsigniertem Zertifikat; der Client vertraut der Server-CA explizit
Das Entscheidende
ExecuteProtocol nimmt ein ganzes Protokoll als JSON entgegen und orchestriert den Geräte-Lifecycle selbst (INIT davor, WORKFLOW_SHUTDOWN danach). Euer Client schickt nur Body-Schritte.
MODUL 01

Setup 10 min

1 · FeliX starten (läuft er schon, weiter zu Schritt 2):

cd "/Users/marcelweissgerber/Downloads/icm-lh-development 2"
docker compose -f docker-compose.local.yml -f docker-compose.local.mac.yml up -d

Ports: 5108 REST/UI · 5109 CASE-gRPC · 5111 SiLA2 (TLS). Kontrolle: localhost:5108/sila-console — dort seht ihr später jeden Call eures Clients live.

2 · CA-Zertifikat holen — der Trust-Anker für den untrusted-Modus (der Server generiert es beim ersten Start selbst):

docker cp icm-felix-local:/opt/icm/ca.crt .

3 · Python-Umgebung — die offizielle SiLA2-Referenzimplementierung:

python3 -m venv silaenv && source silaenv/bin/activate
pip install sila2
Standard-Exkurs
Ein konformer Server verteilt die CA auch über die mDNS-TXT-Records seines _sila._tcp-Advertisements — Clients mit Discovery brauchen den Datei-Umweg nicht.
MODUL 02

Verbinden + Discovery 10 min

from sila2.client import SilaClient

ca = open("ca.crt", "rb").read()
client = SilaClient("127.0.0.1", 5111, root_certs=ca)

print(client.SiLAService.ServerName.get())    # ICM FeliX SiLA2 Server
print(client.SiLAService.ServerUUID.get())    # f91b13d1-… (stabil über Neustarts)
print(client.SiLAService.ImplementedFeatures.get())
Der Kern des SiLA-Modells
Die Bibliothek hat den FeliX nie gesehen — und kennt ihn trotzdem: sie lädt zur Laufzeit die Feature-XMLs via GetFeatureDefinition und generiert daraus den Client. client.LiquidHandlingController existiert ab jetzt einfach.

Checkpoint: Beide FQFIs erscheinen, und in der /sila-console tauchen eure Calls mit Peer-Adresse und user-agent: grpc-python… auf.

MODUL 03

Der Action-Katalog 5 min

Welche Aktionen ExecuteActivity versteht, sagt die Property ActivitySchema — identisch mit GET /api/activity/schema, eine Quelle für REST und SiLA:

import json
lhc = client.LiquidHandlingController
schema = json.loads(lhc.ActivitySchema.get())
print(schema["schemaVersion"], len(schema["actions"]))   # 2.11  38

Jede Action bringt Parameter-Namen, Datentypen und kopfspezifische Volumen-Ranges mit — euer Client validiert Protokolle, bevor das Gerät sie sieht.

MODUL 04

Observable Commands verstehen 10 min

import time
cmd = lhc.ExecuteActivity("WAIT", json.dumps([{"Key": "waitTime", "Value": "1"}]), True)
print(cmd.execution_uuid)          # sofort da — die Aktion läuft im Hintergrund
while not cmd.done: time.sleep(0.3)
result = cmd.get_responses()       # ChecklistJson mit dem Step-Ergebnis

Die Observable-Command-Trias: ① sofortige Bestätigung mit UUID · ② Info-Stream (intern, speist cmd.done) · ③ Ergebnis-Abruf, erst wenn fertig.

MODUL 05

Ganze Protokolle: ExecuteProtocol 15 min

Der eigentliche Arbeitsmodus — nur Body-Schritte, den Rest macht der Server:

protocol = {"steps": [
    {"activity": "MOVE", "parameters": [{"Key": "deckPosition", "Value": "7"},
                                    {"Key": "zReference", "Value": "Topmost"}]},
    {"activity": "WAIT", "parameters": [{"Key": "waitTime", "Value": "1"}]},
]}
cmd = lhc.ExecuteProtocol(json.dumps(protocol), True)   # True = simuliert
while not cmd.done: time.sleep(0.5)
for step in json.loads(cmd.get_responses().StepResultsJson):
    print(step["Step"], step["Activity"], step["Success"])
1 INIT True · 2 MOVE True · 3 WAIT True · 4 WORKFLOW_SHUTDOWN True

Der Server validiert vor dem ersten Gerätekommando: unbekannte Typen und Lifecycle-Actions im Body werden synchron als ValidationError abgelehnt.

MODUL 06

Bedienhinweise: Server pausiert, ihr quittiert 20 min

USER_NOTIFICATION und USER_INPUT sind normale Body-Schritte. Der Server pausiert dort (Gerät bleibt initialisiert!), meldet über den Intermediate-Stream — euer Client quittiert per ConfirmUserPrompt:

import threading

def handle_prompts():
    for intermediate in cmd.subscribe_to_intermediate_responses():
        prompt = json.loads(intermediate.PromptJson)
        print("BEDIENHINWEIS:", prompt["Message"])
        lhc.ConfirmUserPrompt(str(cmd.execution_uuid), prompt["PromptId"], "OK")

threading.Thread(target=handle_prompts, daemon=True).start()
Schritt 3 USER_NOTIFICATION: OK — Quittung 'OK' um 2026-08-06T07:36:46.335…+00:00
Evidenz — auf beiden Seiten
Euer Client besitzt den Zeitstempel (er hat den Dialog gezeigt), der Server persistiert dieselbe Quittung im Step-Ergebnis. „Bediener hat um 07:36 bestätigt, dass Spalte 1 entfernt wurde" existiert im Client-Record und im Server-Ergebnis. Unbeantwortete Hinweise → Timeout (Sila:UserPromptTimeoutSeconds, Default 300 s) → ProtocolFailed, Teardown läuft trotzdem.
MODUL 07

Fehlerbilder lesen 10 min

SiLA-Fehler sind typisiert und kommen in Python als Exceptions an:

FehlerWannWo
ValidationErrorUnbekannte Activity, Lifecycle im Body, kaputtes JSONsynchron beim Aufruf
DefinedExecutionError: ProtocolFailedStep-Fehler (999999 µL → „outside [0.1, 1000.0]"), Prompt-Timeoutbei get_responses()
DefinedExecutionError: UnknownPromptConfirmUserPrompt mit falscher/abgelaufener Idsynchron
MODUL 08

Der fertige Client 10 min

Alles zusammen — CLI, Schema-Vorab-Validierung, interaktive Quittung (oder --auto für Skripte), Ergebnis-Report: tools/masterclass/felix_client.py (~100 Zeilen, im Repo).

cd tools/masterclass
docker cp icm-felix-local:/opt/icm/ca.crt .
python felix_client.py beispiel-protokoll.json --api-key KEY              # interaktiv
python felix_client.py beispiel-protokoll.json --api-key KEY --auto LOT-42 # unbeaufsichtigt

--api-key ist seit Modul 10 Pflicht für schreibende Kommandos: der Client meldet sich damit über das SiLA2-Core-Feature AuthenticationService an und hängt das erhaltene Token an ExecuteProtocol und ConfirmUserPrompt.

Verbunden: ICM FeliX SiLA2 Server (UUID f91b13d1-6142-47e7-b806-0f49723b6fc7)
Schema v2.11: alle 4 Schritte bekannt
>>> BEDIENHINWEIS (Schritt 3): Bitte Spalte 1 aus dem RoboTipTray entfernen. → quittiert 'OK'
>>> BEDIENHINWEIS (Schritt 5): Bitte die Lot-Nummer erfassen. → quittiert 'LOT-42'
Schritt 1 INIT: OK … Schritt 6 WORKFLOW_SHUTDOWN: OK
Damit habt ihr
Discovery, Schema-Validierung, Protokoll-Ausführung mit server-seitigem Lifecycle, Bedienhinweis-Quittung mit beidseitiger Evidenz und typisiertes Fehler-Handling — die komplette Fläche, die auch briefly nutzt.
MODUL 09

Live-Lab: Python-Sandbox gegen das echte FeliX

Hier läuft echtes CPython im Browser (Pyodide, von dieser Domain geladen — kein Server bei uns führt euren Code aus). Das Modul felix ist die Brücke: es spricht dieselben SiLA2-Aufrufe wie der Python-Client aus Modul 08, nur über gRPC-Web zu einem FeliX auf der Testing-Umgebung. print() und Tracebacks landen in der Konsole, Bedienhinweise erscheinen als Dialog.

nicht verbunden Python: nicht geladen
Konsole — print(), Tracebacks und jeder gRPC-Call.
Nach dem Verbinden: Server-Identität, Features und FDL-Definitionen.
Klick auf das i einer Activity zeigt hier Parameter, Typ, Default und Range.
BEDIENHINWEIS

MODUL 10

Authentifizierung: wer darf das Gerät bewegen? 10 min

Bis hierher konnte jeder, der den Port erreicht, das Gerät bewegen. Seit dem Auth-Ausbau verlangt FeliX für die schreibenden Kommandos — ConnectDevice, ExecuteActivity, ExecuteProtocol, ConfirmUserPrompt — ein AccessToken. SiLA2 bringt dafür zwei Core-Features mit, beide implementiert FeliX über die offizielle Referenz:

Drei Regeln, die man wissen muss
1. Die Lese-Fläche bleibt offen: Discovery, GetFeatureDefinition, ActivitySchema, Subscribe_DeviceState funktionieren ohne Token — so will es der Standard (ein SiLA-Server muss auffindbar bleiben). 2. Auf das SiLAService-Feature dürfen Clients das Metadatum nicht mitschicken — der Server bricht solche Aufrufe ab (Spec). 3. Bei Observable Commands gehört das Token an die Initiation; die Folge-Aufrufe (_Info/_Result) laufen über die CommandExecutionUUID, die es nur mit gültigem Token gab.

In der Sandbox aus Modul 09: API-Schlüssel oben rechts eintragen (Login passiert dann automatisch beim ersten schreibenden Kommando) oder explizit im Code:

await server.login("EUER-API-SCHLUESSEL")   # Token holen
ergebnis = await server.execute_protocol(steps)  # Token reist als Metadatum mit
await server.logout()                        # Token verwerfen

Ohne Token antwortet der Server mit einem typisierten SiLA-Fehler (InvalidMetadata bzw. nach Logout InvalidAccessToken) — probiert Beispiel 7 aus der Beispiel-Liste. Ein falscher Schlüssel beim Login ergibt AuthenticationFailed. Alle drei Fehlerbilder lest ihr wie in Modul 07.

ANHANG

Troubleshooting & Wie weiter

ICM FeliX · Branch feature/sila2-server · verifiziert 2026-08-06 · kanonische Quelle: documentation/masterclass-sila2-python-client.md im Repo