Zum Inhalt springen

Die Skript-Schnittstelle

Skripte sind Python. TrakState führt sie in einem eigenen Prozess aus — ohne Datenbank, ohne Zugriff auf das Dateisystem der Anlage, ins Netz nur so weit, wie Ihre Installation es erlaubt. Alles, was hinein- und hinausgeht, geht über die wenigen Namen auf dieser Seite — Tags, die Uhr, der Assistent und die Dateiablage Ihrer Installation.

Alles, was TrakState anbietet, hängt unter system.. Tippen Sie system. und den Punkt, dann zeigt Ihnen der Editor den Rest. Sie müssen keinen einzigen Namen kennen, um zu erfahren, welche es gibt — Sie müssen ein Wort kennen.

wert = system.tag.read("Press1.Cycle")
system.tag.write("Press1.Setpoint", 42)

Die Gliederung sagt selbst etwas: system.tag.* ist die Tag-Schicht, system.time.* die Uhr.

Unter system. steht nur, was von uns kommt. round(), json, datetime, str sind Python und bleiben Python — ein system.util.round gibt es nicht und wird es nicht geben. Was in jedem Python-Handbuch steht, wickeln wir nicht ein; sonst wäre system. eine Behauptung darüber, was uns gehört, und Sie suchten unsere Hilfe nach etwas ab, das dort nie stehen wird.

Blank bleibt, was der Vertrag Ihres Skripts ist: inputs, params, result, on_read, on_write. Das ist keine Bibliothek, die Sie aufrufen, sondern die Form, in der Ihr Skript sich anbietet. system.result wäre kein besserer Name für dieselbe Sache, sondern eine falsche Aussage darüber, was sie ist.

Ein Vertipper endet deshalb nicht in einem Rätsel, sondern in einer Antwort auf die Frage, die Sie tatsächlich haben — was es denn stattdessen gibt:

AttributeError: no 'raed' here - this namespace has: read, write

Die früheren Namen read_tag und write_tag gibt es nicht mehr, ersatzlos und ohne Übergangsfrist. Ein Skript, das sie noch aufruft, scheitert mit einem NameError. Ersetzen Sie sie durch system.tag.read() und system.tag.write().

Welche Form Sie haben, entscheidet TrakState daran, ob Sie on_read oder on_write schreiben. Sie stellen das nirgends ein.

Kein def on_read. Ihr Code ist der Rumpf einer Funktion — return funktioniert also, und wer nichts zurückgibt, dessen Variable result ist das Ergebnis.

# Der Rueckgabewert ist der Wert des Tags.
return inputs["good"] + inputs["scrap"]
# Gleichwertig, ohne return:
result = inputs["good"] + inputs["scrap"]

Ein Tag in dieser Form ist nur lesbar. Ein Schreibversuch darauf wird abgelehnt, nicht still verworfen.

Sie schreiben def on_read() und/oder def on_write(value). Der übrige Code steht dann auf Modulebene und läuft bei jedem Aufruf, bevor der Haken drankommt — das ist das normale Verhalten eines Python-Moduls.

INCH = 25.4
def on_read():
return inputs["length_mm"] / INCH
def on_write(value):
# Was der Setter zurueckgibt, wird der eigene Wert dieses Tags.
system.tag.write("Press1.Length_mm", value * INCH)
return value

Ein Tag mit on_write ist schreibbar, eines ohne nicht. Ein Skript, das nur on_write hat, liefert beim Lesen nichts — es hat gesagt, was es tut, und Lesen war nicht dabei.

Die Haken heißen on_read/on_write und nicht get/set, weil set ein Python-Builtin ist. Wer es überschreibt, macht ein späteres set([1, 2]) im selben Skript kaputt.

Name Aufruf Bedeutung
system.tag.read system.tag.read(path) Liest ein Tag sofort, nach seinem Pfad. Scheitert laut, statt None zu liefern — ein still fehlender Wert wird weiter unten zu einer falschen Zahl.
system.tag.write system.tag.write(path, value) Beantragt eine Schreibung. Aus dem Skript geht nichts hinaus: der Wirt führt die Schreibungen danach aus — oder eben nicht, und genau das ist ein Probelauf.
system.time.plant_now system.time.plant_now() Die Zeit auf der Uhr, nach der die Anlage läuft, mit ihrem Zeitversatz. Nicht dasselbe wie datetime.now(), das die Zone der ausführenden Maschine ist.
system.ai.ask system.ai.ask(prompt, data=None) Fragt das Sprachmodell und liefert seine Antwort als Text. Die Zahlen gehören in data — das Modell formuliert, erklärt und schlägt vor, es rechnet nicht. Nur in Skript-Objekten; jede Frage zählt gegen das KI-Budget.
system.file.list system.file.list(folder, pattern=None) Die Dateien eines Ordners der Dateiablage (nicht seiner Unterordner), jede als Wörterbuch mit id, folder, name, size, media_type, sha256, source, created_at. pattern im Shell-Stil ("*.zip"), Groß- und Kleinschreibung zählt nicht. Nur in Skript-Objekten; jeder Ordner außer Chat/.
system.file.read system.file.read(file_id) Kopiert eine abgelegte Datei in diesen Lauf und liefert den lokalen Pfad — öffnen Sie ihn wie jede Datei. Je Lauf in Bytes und Dateien begrenzt; die Kopie ist mit dem Lauf weg.
system.file.write system.file.write(folder, name, bytes_or_path) Legt eine Datei ab: Bytes, oder der Pfad einer Datei, die das Skript geschrieben hat. Text braucht vorher .encode("utf-8") — ein str wird als Pfad gelesen. Im Probelauf wird nichts abgelegt.
system.file.move system.file.move(file_id, folder) Verschiebt eine abgelegte Datei in einen anderen Ordner; ihre ID bleibt. Im Probelauf wird nichts verschoben.
system.file.claim system.file.claim(file_id, key="default") Nimmt eine Datei für diesen Lauf, atomar: True — einlesen; False — schon eingelesen (dieselben Bytes zählen) oder ein anderer Lauf liest sie gerade ein. Wird verarbeitet, wenn der Lauf und alle seine Schreibvorgänge gelingen, und freigegeben, wenn er scheitert. Die Prüfung, mit der ein Import beginnt.
system.file.processed system.file.processed(file_id, key="default") True, wenn diese Datei — oder dieselben Bytes unter anderem Namen oder nach einem Verschieben — schon unter key verarbeitet ist, oder ein anderer Lauf sie gerade hält. Ein Blick, keine Sperre — entscheiden Sie mit system.file.claim.
system.file.mark_processed system.file.mark_processed(file_id, key="default", note=None) Merkt sich, dass eine Datei erledigt ist. Wird gesammelt wie system.tag.write: abgelegt nach dem Lauf, und nur wenn jeder Schreibvorgang des Laufs durchging.
system.push.send system.push.send(role, title, body, path=None, tag=None) Beantragt eine Push-Benachrichtigung an alle, die eine Rolle innehaben — auf dem Telefon und im Browser gleichermaßen. Wird gesammelt wie system.tag.write: der Wirt versendet sie nach dem Lauf, oder gar nicht, im Probelauf.
system.log.info system.log.info(text) Eine Zeile für das Diagnose-Panel. Bei einem Lauf, den niemand von Hand gestartet hat, sehen sie die Administratoren, mit dem Pfad des Skripts – alle anderen sehen einen kurzen Hinweis, dass eine Warnung oder ein Fehler protokolliert wurde; bei einem Lauf von Hand und bei Ausprobieren der, der den Knopf gedrückt hat (Ausprobieren zeigt sie zusätzlich unter dem Ergebnis). Bei einem Skript-Tag sehen sie die Administratoren mit dem Pfad des Tags – die Zeilen eines Getters höchstens einmal je Minute, weil er bei jedem Lesen läuft. Bleibt auch erhalten, wenn der Lauf scheitert; höchstens 20 Zeilen je Lauf.
system.log.warn system.log.warn(text) Wie system.log.info, Stufe Warnung — etwas ging schief, und das Skript hat weitergemacht.
system.log.error system.log.error(text) Wie system.log.info, Stufe Fehler. Der Weg, einen Fehler zu melden, den das Skript selbst abgefangen hat — ein nicht abgefangener verwirft alle Schreibvorgänge des Laufs.
inputs inputs["name"] Ihre gebundenen Eingänge, unter den Namen aus der Eingangsliste. Der nachvollziehbare Weg, von einem Tag abzuhängen.
params params["name"] Was der Auslöser wusste. Ein Verbindungsereignis übergibt dem Skript den Namen der Verbindung, die abgerissen ist; Datei angekommen übergibt file_id, folder und name der neuen Datei.
result result = ... Der Wert, wenn Sie keinen zurückgeben. Nur in der einfachen Form — ein Skript mit Haken antwortet durch sie.
on_read def on_read(): Liefert den Wert des Tags. Wer ihn schreibt, verschiebt seinen übrigen Code auf Modulebene, wo er bei jedem Aufruf läuft, vor dem Haken.
on_write def on_write(value): Nimmt einen geschriebenen Wert entgegen und macht das Tag überhaupt erst schreibbar. Was der Haken zurückgibt, wird der Wert des Tags.
print print(...) Geht ins Ausführungsprotokoll, nicht ins Ergebnis. Die beiden Kanäle sind mit Absicht getrennt.

Mehr kommt von uns nicht. Alles Übrige in Ihrem Skript ist Python.

Dateien: Lieferungen einlesen, ohne dass jemand klickt

Abschnitt betitelt „Dateien: Lieferungen einlesen, ohne dass jemand klickt“

Eine Lieferung, die immer wieder kommt — jeden Morgen ein Zip je Fertigungslinie, ein Export, den eine Maschine jede Schicht ablegt — liest ein Skript ein, nicht jemand von Hand. system.file erreicht die Dateiablage (Einstellungen › Dateien); der Auslöser Datei angekommen startet das Skript für jede neue Datei in einem Ordner, egal wer sie abgelegt hat: jemand auf der Seite Dateien, eine Maschine mit API-Token, der Assistent oder ein anderes Skript.

import csv, io, zipfile
def run_import(folder):
imported = skipped = 0
for f in system.file.list(folder, "*.zip"):
if not system.file.claim(f["id"]):
skipped += 1
continue
with zipfile.ZipFile(system.file.read(f["id"])) as z:
for entry in z.namelist():
if entry.endswith(".csv"):
text = io.TextIOWrapper(z.open(entry), encoding="utf-8")
for row in csv.DictReader(text, delimiter=";"):
system.tag.write("Production.Records", {"Line": row["line"], "Pieces": int(row["pieces"])})
imported += 1
system.tag.write("Import.Status", f"{imported} Zeilen eingelesen, {skipped} Lieferungen schon eingelesen")

Was ein Skript sieht: die ganze Dateiablage außer den privaten Ordnern Chat/, Users/ und Sessions/. Hinter einem Skript steht keine Person, und dort liegt, was einer Person oder einem Gespräch gehört — ein Skript kann dort nichts auflisten, lesen, schreiben, verschieben oder beanspruchen, eine Datei-ID von dort liest sich wie „keine solche Datei“, und ein Auslöser Datei angekommen kann sie nicht beobachten. Team/ und alle anderen Ordner sind gemeinsam.

Zweimal laufen ist ungefährlich. system.file.claim() nimmt die Datei in einem atomaren Schritt für diesen Lauf und vergleicht den Inhalt: dieselbe Lieferung noch einmal hochgeladen — unter anderem Namen, oder nachdem die erste Kopie verschoben wurde — gilt als erledigt, und zwei Läufe, die im selben Moment auf einer Lieferung starten (das Skript mit Datei angekommen und das Knopf-Skript), können sie nicht beide bekommen. Der Anspruch wird „verarbeitet“, wenn der Lauf und alle seine Schreibvorgänge durchgingen, und freigegeben, wenn der Lauf scheitert, sodass der nächste Lauf sie einliest; der Anspruch eines abgestürzten Laufs verfällt nach seiner Laufzeit plus zehn Minuten. system.file.mark_processed() markiert eine Datei ohne Anspruch (oder ergänzt eine Notiz), gesammelt wie ein Schreibvorgang.

Erst an einer echten Lieferung ausprobieren. Ausprobieren (und der Probelauf des Assistenten) liest die echte Datei und legt nichts ab, verschiebt nichts und markiert nichts — das Ergebnis listet, was geschehen wäre.

Grenzen. system.file.write hält sich an Dateigröße und Kontingent der Ablage. system.file.read ist je Lauf begrenzt, in Bytes (512 MB, sofern die Installation nicht SCRIPT_FILE_READ_BUDGET_MB setzt) und in Dateien (1.000); Laufzeit-, Speicher- und Dateigrößen-Grenzen des Laufs gelten zusätzlich. Ein Import braucht meist mehr als die voreingestellten fünf Sekunden: geben Sie ihm bis zu 60 s.

Ein Knopf auf einem Board braucht kein eigenes Widget: ein Sollwert-Widget schreibt ein Zahlen-Tag, ein Skript mit dem Auslöser Tag-Änderung auf diesem Tag liest ein, und das Skript schreibt seinen Stand („42 Zeilen eingelesen, 3 Dubletten übersprungen“ oder den Fehler) in ein Text-Tag, das eine KPI-Kachel anzeigt. Schreiben Sie den Import einmal als Bibliothek und starten Sie ihn aus beiden Skripten — dem Knopf-Skript und dem mit Datei angekommen.

inputs oder system.tag.read() — nehmen Sie inputs

Abschnitt betitelt „inputs oder system.tag.read() — nehmen Sie inputs“

Ein gebundener Eingang ist der nachvollziehbare Weg: TrakState weiß dann, dass dieses Skript an diesem Tag hängt, und kann es Ihnen zeigen.

# Gebunden: der Name links kommt aus Ihrer Eingangsliste.
parts = inputs["part_count"]

system.tag.read() ist bequem und die schwächere Wahl: nichts außerhalb Ihres Textes weiß, dass das Skript von diesem Tag abhängt. Wird der Pfad umbenannt, geht das Skript kaputt, und niemand wird gewarnt.

# Ungebunden: nur nehmen, wenn der Pfad erst zur Laufzeit feststeht.
line = params["line"]
parts = system.tag.read("Plant." + line + ".PartCount")

system.tag.read() scheitert laut, wenn es den Wert nicht gibt — es liefert kein None. Ein still fehlender Wert wird sonst weiter unten zu einer falschen Zahl.

system.tag.write() sendet nichts. Es hängt Ihren Wunsch an eine Liste, die der Wirt danach abarbeitet — oder eben nicht, und genau das ist ein Probelauf (Dry run): derselbe Code, dieselben Berechnungen, die Schreibungen unterbleiben.

Zwei Folgen, die Sie kennen sollten:

  • Ein system.tag.write() ist nach dem Aufruf noch nicht passiert. Ein system.tag.read() auf denselben Pfad in derselben Ausführung liefert weiter den alten Wert.
  • Die Rechteprüfung bleibt beim Wirt. Ein Skript kann nichts schreiben, was der Auslöser nicht schreiben dürfte.

Push-Benachrichtigungen werden gesammelt, nicht versendet

Abschnitt betitelt „Push-Benachrichtigungen werden gesammelt, nicht versendet“

system.push.send() funktioniert genau wie system.tag.write(): es sendet selbst nichts, sondern hängt Ihren Wunsch an eine Liste, die der Wirt danach abarbeitet — oder im Probelauf gar nicht, wo das Panel “würde senden” anzeigt und kein Telefon klingelt.

system.push.send("qualitaet", "Presse1 steht", "Die Presse steht seit 5 Minuten.",
path="/alarms/presse1", tag="presse1-steht")
  • role ist ein Rollenname, keine Person — jeder, der die Rolle innehat, bekommt die Benachrichtigung, auf jedem registrierten Gerät (Browser-Tab und TrakState-App gleichermaßen).
  • path ist das Ziel, an dem ein Antippen der Benachrichtigung landen soll, relativ zur eigenen Instanz.
  • tag fasst zusammengehörige Benachrichtigungen zusammen, sodass eine spätere eine frühere auf dem Bildschirm ersetzt statt sich anzuhäufen; der Versand nutzt es außerdem, um dieselbe Benachrichtigung nicht zweimal innerhalb einer Minute zu senden.
  • Ein Lauf ist auf 10 Aufrufe von system.push.send() begrenzt; der Rest wird als Fehler am Lauf gemeldet statt versendet.

Ein Skript schreibt einen Datensatz genauso wie einen Wert: über das Tag.

system.tag.write("Process.Records", {
"At": system.time.plant_now().isoformat(),
"Machine": "ERA-01",
"Area": 3.6,
})

So werden Maschinendaten zu auswertbaren Zeilen: ein Konnektor oder ein anderes Skript legt die Meldung der Maschine auf ein Tag, Ihr Skript macht eine Zeile daraus, und das Datensatz-Tag ist die einzige Tür, durch die sie geht. Direkt in die Datenbank schreibt nichts.

Zwei Dinge, die man kennen sollte — beide kosten einen Nachmittag, wenn man ihnen unvorbereitet begegnet:

  • Die Zeile wird Ihrem Skript zugeschrieben, mit seinem Pfad — script:Decoders.ERA-01 — und stattdessen der Person, wenn sie auf Ausführen gedrückt hat. Ein Datensatz ohne Urheber wird abgewiesen, denn eine Zeile ohne Herkunft bricht die Spur, die die Erfassung führen muss. Das stellen Sie nicht ein und können es nicht: es kommt daher, wer oder was tatsächlich gelaufen ist.
  • Werte müssen einfach sein: Text, Zahlen, wahr/falsch. system.time.plant_now() liefert ein datetime, und ein datetime in einem geschriebenen Wert übersteht den Weg nicht — schreiben Sie .isoformat() wie oben. Ein Wert, der nicht mitreisen kann, lässt den ganzen Lauf scheitern, nicht nur das Feld.

Die Zeile hält außerdem fest, wie sie hereinkam: script für diese, daneben manual für etwas, das jemand getippt hat, und assistant für etwas, das der Assistent erfasst hat. Eine Zahl, die falsch aussieht, lässt sich viel leichter erklären, wenn die Zeile sagt, woher sie kommt.

system.time.plant_now() liefert die Plantzeit — als datetime, das seine Zone kennt.

jetzt = system.time.plant_now()
schichtbeginn = jetzt.replace(hour=6, minute=0, second=0, microsecond=0)

datetime.now() ist etwas anderes: die Zeitzone der Maschine, auf der Ihr Skript ausgeführt wird. Auf einer betriebenen Instanz ist das zuverlässig UTC. Ein Skript, das der Zeitplan um „Montag 06:00” Plantzeit startet, rechnet dann mit einer anderen Uhr als sein eigener Auslöser. Solange beide Zonen zufällig übereinstimmen, fällt das nicht auf — es fällt bei der Schichtübergabe um sechs Uhr morgens auf.

Deshalb ist es eine benannte Funktion und keine Zeitzone im Prozess: eine Umgebungsvariable hätte datetime.now() still umgedeutet und damit auch jedes Skript geändert, das bewusst UTC rechnet. Für alles außer der Wanduhr der Anlage bleibt datetime Pythons datetime.

system.ai.ask(prompt, data) stellt dem Sprachmodell eine Frage und liefert die Antwort als Text — dasselbe Modell, das auch der Assistent benutzt.

figures = system.tag.read("KPI.ShiftSummary")
report = system.ai.ask("Fasse diese Schicht in drei Sätzen zusammen und nenne Auffälligkeiten.", figures)
system.tag.write("Reports.Records", {"At": system.time.plant_now().isoformat(), "Text": report})

Die Zahlen rechnet Ihr Skript, nicht das Modell. Geben Sie ihm in data mit, was eine Abfrage oder Ihr Code ermittelt hat. Das Modell ist angewiesen, nur diese Zahlen zu verwenden und zu sagen, wenn etwas fehlt. Einen Bericht, dessen Zahlen ein Modell erfunden hat, glaubt ausgerechnet der, der nicht nachprüft.

Die Antwort kommt in der Oberflächensprache der Person, der der Lauf gehört — wer „Jetzt starten“ gedrückt oder den auslösenden Wert geschrieben hat, und bei einem Lauf, den niemand angestoßen hat (Intervall, Zeitplan, Ereignisse), wer das Skript zuletzt gespeichert hat; ohne beides Englisch. Die Sprache Ihrer Frage entscheidet das nicht, denn wer das Skript schreibt, liest den Bericht selten.

Drei Dinge, die Sie wissen sollten:

  • Nur in Skript-Objekten — auf einem Auslöser oder von Hand gestartet. Ein Skript-Tag wird bei jedem Lesen ausgewertet, und ein Board, das regelmäßig nachliest, würde aus einer Kachel eine Modellrechnung machen. Dort scheitert der Aufruf mit einer Meldung, die das sagt.
  • Die Wartezeit auf das Modell zählt nicht zum Zeitlimit. Eine Frage wartet höchstens 60 Sekunden; das Zeitlimit gilt für Ihren eigenen Code.
  • Jede Frage kostet. Sie zählt gegen das KI-Budget des Mandanten und steht im Verbrauch unter script.ask. Mehr als 40 000 Zeichen Daten werden abgelehnt — verdichten Sie vorher in einer Abfrage.

In vielen Skript-Umgebungen ist die Standardausgabe der Ergebniskanal — das erste print(), mit dem Sie nachsehen wollen, wie ein Wert aussieht, zerstört dort das Ergebnis. Hier nicht: Ergebnis und Ausgabe laufen getrennt. Ihre Ausgaben stehen im Ausführungsprotokoll unter dem Lauf.

print("good:", inputs["good"], "scrap:", inputs["scrap"])

print() bleibt im Ausführungsprotokoll. Was im Diagnose-Panel ankommen soll – eine Warnung, ein Fehler, den das Skript selbst abgefangen hat – geht über system.log.info/warn/error (Tabelle oben).

Die Standardbibliothek von Python — unter anderem:

json math time datetime zoneinfo calendar decimal fractions statistics
random re string textwrap collections itertools functools operator
typing dataclasses enum copy uuid hashlib base64 binascii struct
csv io os sys pathlib bisect heapq abc

Dazu Ihre eigenen Bibliotheken, über ihren Pfad als Modulnamen (siehe Bibliotheken).

Ein Import, der weder eine Ihrer Bibliotheken noch ein bekanntes Standardmodul benennt, wird im Reiter Probleme als Hinweis gemeldet — nicht als Fehler. TrakState kennt die Python-Installation Ihrer Anlage nicht und behauptet deshalb nie, ein Modul gebe es nicht; es sagt nur, dass es hier nicht gefunden wurde.

system müssen Sie nicht importieren. Es steht bereit, bevor Ihre erste Zeile läuft.

Zeitlimit je Lauf 5 Sekunden
Wartezeit auf das Modell zählt nicht zum Zeitlimit; höchstens 60 Sekunden je Frage
Arbeitsspeicher 256 MB (auf Linux; unter Windows nicht erzwingbar)
Ausgabemenge begrenzt — eine Schleife voller print() kann den Speicher nicht füllen
Rückgabewert muss sich als JSON darstellen lassen

Läuft ein Skript in das Zeitlimit, endet der Lauf mit „the script was stopped after 5000 ms”. Das ist kein Fehler in Ihrem Code, sondern seine Laufzeit.

Offen: Das Zeitlimit ist je Skript hinterlegbar, aber die Oberfläche bietet dafür derzeit kein Feld. Bis das nachgezogen ist, gelten die 5 Sekunden.

Ein Fehler wird als Typ, Meldung und Zeilennummer in Ihrem Code gemeldet — nicht in unserem. Auch wenn system.tag.read() scheitert, zeigt die Meldung auf die Zeile, in der Sie es aufgerufen haben.

ZeroDivisionError: division by zero (Zeile 4)