LogicEngine

From CometVisu
Jump to: navigation, search

(zunächst auf deutsch)

Teilprojekt LogicEngine

Die LogicEngine ist das Teilprojekt, welches eine Laufzeitumgebung für logische Abläufe zur Verfügung stellt. Die bisher gewählte Programmiersprache ist Python.

<hier sollte noch eine fluffige Übersicht des Teilprojektes erfolgen>

Projektziele

  • auf Opensource basierende Lösung
  • hochperformant
  • erweiterbar (u.a.) durch offenes Protokoll xPL
  • Ressourcen schonend um auch mit einer beliebig schwachbrüstigen Hardware klarzukommen
  • durch KonfigEditor ohne eine Kommandozeile zu bedienen (DAU benutzbar)
  • auch Elektriker ohne Spezialausbildung (die fasse ich explizit nicht unter DAUs) sollten das System nutzen wollen
  • HS3 ablösen

Projektorganisation

Beteiligte

  • NilS
  • ChrisM
  • Makki

Rollen

(Rollen und Zurodnung habe ich frei erfunden)

  • Architekt (NilsS)
  • Entwickler (ChrisM, Makki)
  • Infrastrukur (ChrisM)
  • Test (alle)

Meilensteine

Also gerne nochmal mein "Zeitplan":

  1. Proof-of-Concept, Grundlegende Funktion implementieren, Gerüst erstellen, d.h. erste Alpha - bei mir privat - DONE
  2. Das ganze etwas reifen lassen, auf etwas mehr Systemen testen, Alpha - mit ein paar wenigen Alpha-Testern/Entwicklern - DONE
  3. Das ganze aufräumen, für interessierte Entwickler nutzbar machen, interne Beta - DONE
  4. Das ganze auf vielen Systemen testen, für interessierte Anwender (im Sinne der CV-Entwicklung, also SI u.ä.) nutzbar machen, öffentliche Beta
  5. Release der initialen Funktionen, für produktiven Einsatz geeignet

Momentan stehen wir am Schritt von 3 nach 4...

Technisches Umfeld

Software

  • Python
  • ECMA (JS) / Lua
  • JSON
  • xPL
  • 1-wire /owfs
  • KNX

Was wir ausgeschlossen haben ist:

  • Alles was mit Java anfängt oder Beans enthält
  • Analog Flash, .Net, i*-App

Hardware/OS

  • Wiregate (oder vergleichbar)
  • am liebsten etwas im REG-Format
  • primär x86, optional vielleicht mal armel, minimum derzeit Geode LX800, 256MB RAM, ein Alix.1D eben
  • Debian stable
  • Windows, centos, susie, WRT, BSD und 500 werden nicht weiter verfolgt. Es darf auch woanders laufen, wenn sich jemand dafür zuständig fühlt es also macht/testet (nicht nur fordert!). Priorität dafür ist aber "-10"
  • Atom E600 Serie (verfügbar voraussichtlich 11.2010 / http://www.heise.de/newsticker/meldung/IDF-Atom-E600-und-Windows-7-fuer-Embedded-Systems-1079219.html ) wie z.B. nanoETXexpress-TT - nanoETXexpress - Computer-on-Modules > COM Express

Infrastruktur

  • Veraltet: SVN (http://openautomation.svn.sourceforge.net/viewvc/openautomation/PyWireGate/) => ist eingerichtet
  • Aktuell: Git (https://github.com/OpenAutomationProject/PyWireGate) => ist eingerichtet
  • automatische nightly builds der DEBs & Beta-Repository - Hiermit soll die Nutzung und Entwicklung für potentielle Mitstreiter so einfach wie möglich zu halten -> gefragt sind hier auch Mitwirkende für Doku, regression-testing etc => offen
  • automatisierte Sanity-Tests beim Paketbau aus dem Repo - Makki hat da aus seinen Erfahrungen mit Wiregate gelernt und da eine genaue Vorstellung davon, wie diese Tests aussehen können => offen

Quelltext

## Copyright (c) 2010, <Vorname> <Name>, All rights reserved.
##
## This program is free software; you can redistribute it and/or modify it under the terms
## of the GNU General Public License as published by the Free Software Foundation; either
## version 3 of the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
## without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
## See the GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along with this program;
## if not, see <http://www.gnu.de/documents/gpl-3.0.de.html>.
  • Kommentare und Inline-Doku werden in englischer abgefaßt
  • CRLF - da die primäre Zielplattform *nix ist und jeder Editor ausser Notepad (ist das mit Win7 endlich mal gefixed?) damit klarkommt, sollte "\n" verwendet werden

Architektur

Module

  • cometserver
  • pywiregate
  • webserver
  • openvpn
  • datenbank

interne Kommunikation

Alle Module kommunizieren intern über die WireGate Instanz, die allen beim Constructor mit übergeben wird. Der Datastore wird das eigentlich weitereichen machen. Logging und Configs werden auch zentral über die WG Instanz gemacht. Ansonsten müssen die Module eigentlich (mag später ausnahmen geben) nix voneinander wissen

externe Kommunikation

Als Protokoll für die externe Kommunkation wird JSON (www.json.org) verwendet.

Verarbeitungsmodus

  • Kontinuierlich (Größe kann zu jedem Zeitpunkt befragt werden und kann jedes mal einen anderen Wert haben. Ziemlich unnützes Beispiel könnte CPU Load sein)
  • Event (eine zeitlich diskrete Größe, die beim Auftreten Aktionen auslösen kann). Z.B. der 1-Wire muss trotzdem gepollt werden, aber das ist dann eben job des connector-Threads
  • Ggf. Zustand (Info, wo ein Zustandsautomat gerade hängt).

Diese Arten können natürlich Verbindungen miteinander haben. Ein Schwellwertschalter auf einen kontinuierlichen Wert könnte Events verschicken. Ein Memory könnte sich den letzten Wert eines Events merken und kontinuierlich bereitstellen. Ein Zustand ist eigentlich ein kontinuierlicher Wert, der durch Events sich ändern kann und bei jedem Zustandsübergang neue Events verschickt.

Das würde eine Logik-Engine geben, die sowohl zeitbasiert mit fester Schrittweite arbeitet (nämlich alle Blöcke mit kontinuierlichen Eingängen verarbeiten) als auch mit Function-Calls (jeder getriggerte Event ist ja eigentlich in Function Call).

  • Warum wir einen Zustand brauchen:

Spätestens wenn Du modellbasiert unterwegs bist, wie z.B. bei manchen aufwendigeren Regelungen, geht's eigentlich nicht mehr ohne.

Da kontinuierlich natürlich letztendlich auf digitalen Systemen nicht möglich ist, muss man sich behelfen. Man tastet das System mit einer passenden Schrittweite ab. Die Anwendungen meines täglichen Jobs machen das auf PowerPC und x86 Hardware jede Millisekunde (Ausnahmen auch mit 10 kHz), die Steuergräte sind im schnellsten Task (Ausnahmen abgesehen) auf 10 ms auf embedded Prozessoren. (Wenn man Offline, d.h. ohne Echtzeit, Simuliert, dann nimmt man oftmals keine feste Schrittweite sondern eine variable - die macht aber alles noch komplizierter)

Nun kann man natürlich sagen, dass man einfach ein Event/Trigger definiert, dass alle 10 ms aufgerufen wird. Dabei gibt's aber dann die Frage der Aufrufreihenfolge zu klären. Gerade bei Regelkreisen oder bei physikalischer Modellierung ist es notwendig Werte zurückzuführen, d.h. der Ausgang eines Blocks wird wieder, nach ein paar anderen Blöcken, vorne eingespeist. Werden nun die Blöcke in der falschen Reihenfolge berechnet, werden die Signale teilweise unzulässig verzögert und das ganze kann instabil werden (mathematisch beweisbar). Folglich muss man über einen geeigneten Algorithmus die Aufrufreihenfolge festlegen (wenn man das nicht dem Anwender überlassen will - womit die meisten überfordert sein dürften).

Aber wie willst Du einen PI-Regler implementieren, wenn es keine kontinuierliche (bzw. zeitlich getaktete) Aufrufe gibt? D.h. kontinuierlich können interne Variablen sein. Spätestens wenn man ein Beobachtermodell der Raumtemperaturen machen will, dass von den einzelnen Sensoren gestützt wird und das auch Temperaturen berechnet, wo keine Fühler verbaut sind, wird man ohne (quasi-)kontinuierliche Berechnung verzweifeln...


Funtionsweise eines Connectors

Das DataStoreObj enthällt eine Funktion setValue, die mit der send Funktion des passenden Connector verbunden ist. Das heißt ohne zu wissen was dran hängt, kann eine Logik oder Visu ein obj.sendValue(blub) machen ohne zu wissen ob das geht oder nicht. Die Connectoren schreiben ihre Werte die sie empfangen auf die Datastores, entweder die bekannten oder die unbekannten. So können neue GAs/Sensoren ... was auch immer schnell erkannt werden.

Alle Connectoren sollten logging und statistiken bieten, die sie selbst verwalten und Zentral in einheitlichem Format ablegen. Miniaturansicht angehängter Grafiken WireGate.py Brainstorming-wglogik.png

So ich hab jetzt mal einen DATASTORE eingerichtet worin initial jetzt die eibgaconf eingelesen wird. Dabei hab ich gerade mal beschlossen DPT als Standardtype zu definieren, da gibts ja eigentlich für alle was. Damit wandert das decodeDPT und seine dazugehörigen funktionen aus dem knx_Connector raus. Eine autoerkennung für unbekannte könnte dort auch gleich mit einfließen.

Mir ist als Idee gekommen, im Datastoreobjekt (iko) eine Liste mit den letzten änderungen (zeit,src,val) zu führen. z.B. 200 Einträge, dann kann man a) gut sehen ob von einer phy zuviel kommt (konsole) oder aber auch eine Statistik erstellen, wie die sendelast/iko ist.

Ich werde, damit ich die subscriber nich ständig rein/raus schreiben muss, direkt in der funktion DATASTORE.update einen globalen subscriber hinzufügen.

Dort meldet sich der Wartende Client and, und erhällt(der ClientThread, nicht der Client) dann alle Werte, die er mit seiner filterliste vergleicht und in eine queue läd, diese queue wird nach timeout (.1s) wenn sie voll ist, in einem Packet an den Client geschickt. Dieser kann so auch 2,3,4 direkt aufeinanderfolgende Werte in einer Antwort erhalten.

Wenn sich der Client anmeldet, bekommt er während sich die queue füllen kann, einmalit alle in seinem Filter genannten iko's

Das ist einfach Authorization free, zumindest auf iKO/Logik/Connector Basis. Ich kann mir jetzt natürlich eine explicit nicht Webbaiserte schnittstelle einfallen lassen(Konsole ist ja auch da) über die diese Informationen zu bekommen sind, aber was bringt das dann.

a) ---DATASTORE---- -> API-Schnittstelle -> CGI -> httpd(https) ist da das ganze halt nur doppelt gegenüber b) ---DATASTORE---- -> HTTP-Schnittstelle -> httpd(https) Da die API-Schnittstelle ja ebenso gethreaded laufen würde, wie das auch die HTTP-Schnittstelle macht.

Die Daten kommen nunmal von da.

bei Variante a) wäre das jetzt so Client verbindet über lighttpd und führt CGI aus, CGI wird geforked und baut eine Verbindung zur API-Schnittstelle (wohl auch dann ein Socket) auf und muss per kompliziert erstmal sagen, ich bin ich möchte xxx. Da diese Schnittstelle jetzt aber nicht Objekte übernehmen kann, muss alles was dort registriert ist ja erst serialisiert werden um dann wieder vom CGI als JSON zusammengebaut zu werden.

bei Variante b) Client verbindet über lighttpd (reverseproxy) eine Verbindung zur http-schnittstelle, die Authorisiert aufgrund des REMOTE_USER der egal von welcher Authentication im lighttpd gemacht wurde, und gibt direkt das JSON, welches direkt aus den Obejkten erstellt wurde zurück.

Ok jetzt könnte man ja sagen, warum serialisert die API in Variante a) nicht gleich in JSON ?? aber dann wäre die Frage, was a) von b) noch unterscheidet.

Damit wir aber mal bisschen was aus den lokalen Konfigs holen können bau ich mal ein config2datastore.py, damit können dann eibga.conf (oder besser gleich etsexport.csv (wegen UTF) als auch owsensors.conf eingelsene werden.

intere Datenhaltung

Die Visu muss sicherstellen, alle Werte zu haben.

Beispiel, Visu hört auf "foo" und "bar":

  1. Read wird von Visu geschickt und es wird gewartet
  2. Irgendwann kommt "foo"
  3. Backend antwortet mit "foo"
  4. Visu verarbeitet "foo"
  5. Es kommt "bar"
  6. Die Visu hört wieder

=> Die Visu hat plötzlich das Paket "bar" verpasst

Die Lösung ist aber nicht wirklich schwer: Das Backend schickt mit jeder Antwort einen Index (Timestamp, whatever, ...) mit. Bei der nächsten Anfrage der Visu wird genau dieser Index mitgeschickt. Das Backend antwortet nun mit allen (gefilterten) Paketen die seit dem angefallen sind - oder wartet bis eines kommt (oder ein Timeout passiert)

Vgl. übrigens Spec des CometVisu Protokolls

PS: Aus Performance-Gründen ist es wichtig, dass nicht alle Werte übertragen werden, sondern dass es einen Filter gibt. Im Intranet kann das egal sein, bei einer Visu auf'm Handy nicht mehr

DPT versus intere Datentypen (Python)

  • intern werden die Daten in "normalen" Programmiersprachenspezifischen Datenstrukturen gehalten (float/int/string)
  • die jeweiligen Connectoren wandeln in die jeweils nötigen Datentypen um (encodeDPT)
  • die CometVisu nimmt die rohen Daten, damit das Backend die Datentypen (bzw. die Zuordnung zu den GAs) nicht kennen muss.
  • Das pywiregate wird aber sicher eine eibga.conf lesen müssen und können, daher dort sicher eher klartext/string/float (so war das mit den DPTs nicht gemeint, eher so, das man damit auch gleich weiss obs ne Temperatur, Luftfeuchte oder Bananenschalenstärke ist)..

Alles was "nach außen" kommuniziert und/oder sichtbar ist, sollte IMHO ein universelles Datenformat haben - oder zumindest eines, das nicht Python-Version spezifisch ist.

Ergo:

   * Die Konfiguration des ganzen gehört IMHO in eine Text-Datei (vom INI-Format bis zu XML ist viel denkbar)
   * Die Kommunikation zwischen der Logik-Engine und "Plugins" (bzw. eher "Anhängen") sollte IMHO auch Nicht-Python lesbar sein
   * Die Kommunikation zwischen Threads der Logik-Engine darf gerne Python-"Obskur" sein.
   * Das Ablegen von "remanenten" Daten, tja, da wird's schwierig. ICh würde fast zu editierbar tendieren...

Hintergrund ist zum einen die Warbarkeit, und zum anderen die Tool-Komapatabilität. Auch wenn die Logik-Engine Python ist, möchte ich nicht bei der Peripherie zwingend darauf angewiesen sein. Sondern z.B. auch C verwenden können (ohne Python einzubetten...)


Logging

  • Logging hab ich gerade eingebaut und wird auch abonierbar sein
  • Je Konnector KANN das logging individuell in ein separates file und die loglevel eingestellt werden

Allgemeines

  • jeder Wert muss einen Datentyp haben.
  • bitte NICHT intern mit einer Vergewaltigung der KO arbeiten, sondern mit normalen Variablennamen, da KNX nur ein Bus unter vielen ist
  • jedes Objekt wird eine eindeutige ID haben, die im Falle KNX halt in dem Bereich bleibt, das daraus die GA erzeugt werden kann. Allen anderen wird automatisch eine ID (GA die nicht KNX ist) gegeben. So kann selbst KNX eine nicht existente GA 100/2/2 ansprechen (wenn es möchte)

irgendeine Art von ID hat aber nunmal jede Ansammlung.

  • Bestands(schutz) der Perl-Plugins: Die wird es auf einige Sicht natürlich weiter geben! Für manche Sachen finde ich das auch einfacher&schneller zielführend (z.B. Leute, die OO nicht selbsterklärend finden um mal schnell ne Mini-Funktion reinzuschmeissen.. Ist ja absichtlich so "einfach" wie möglich gehalten). Der Plan war ohnehin, die Kernfunktionen (owfs/KNX) in einen schlanken daemon auszulagern (ob nun C oder py is mir egal) und dann nur noch einen kleinen für die Perl-Plugins laufen zu haben.. Mit POE::Wheels habe ich heute glaube ich auch zufällig was gefunden das kaputte Perl-Threading zu vermeiden..

Feature-Liste (grob/fein => ungeordnet)

  • Alles angeschlossenen Systeme sollten in einer eventklasse zusammengefasst werden. Eventklassen: KNX, 1wire, webabfrage, Netzwerk-ports (tcp/udp), VisuClick, Logik weitere .. vom System(monit,openvpn), SIP ....
  • Ko's oder wie auch immer (KNX, interne .... sollten möglichst gleichbehandelt werden, aufbau jeweils als objekt
  • Webserver ... für Visu und Konfiguration + Datenaustausch
  • Webserver ohne Sicherheitheitsfeatures. lediglich Authorization SSL und Auth sollten von apache lighttpd übernommen werden.
  • Diagnose Interface um per commandozeile alle Wert anzuzeigen.
  • Liveanzeige der Werte im Logikeditor.
  • Logiken aktivieren/deaktivieren.
  • Im mittlerer Zukunft kommt xPL und mein Langfrist-Plan ist OPC UA.
  • Konnektoren: KNX,1wire,owfs,xpl..
  • Die Kommunikationsobjekte verwenden als ID ihre Konnektorkennung, die Ihnen von ihrem Konnektor vor ihre
  • Alle Konnektoren senden ihre empfangen Daten in eine Queue,
  • Jeder Connector hat eine Subscriber funktion, in der sich Logiken oder Connectoren einloggen können z.B.
    • receive(self,msg)
    • for func in self.subscriber:
    • func(msg)
    • subscribe(self,func)
    • self.subscriber.append(func)
  • Einen Datenspeicher für alle bekannten KOs sowie einen der alle unbekannten über irgendeinen Connector aufgenommenes KO speichert.
  • Eine ID vom KO vom KNX sähe dann so aus KNX_0111 was 0/1/11 wäre (war zu faul zum rechnern GA dezimal) 1WIRE_28.34324234 irgendwas.
  • In Logik und Visu kann man die dann entweder über ihre ID oder über ihren Namen verknüpfen.
  • XML
  • INI-Datei
  • NoSQL
  • "Bausteine" mit LUA für pywiregate. Das entspringt dem Gedanken, das es für halb-noobs (zu denen ich mich gelegentlich selbst gerne zähle) möglichst einfach sein soll Logikbausteine zu machen. LUA finde ich da eine zumindest bedenkenswerte Sache..
  • interner Ringpuffer für die Werte der internen Events (analog der Erweiterung des eibd so wie ich es verstanden habe). So dass die CometVisu read und ggf. write Requests diesbezüglich erweitert werden können. Schließlich will ich ja auch interne Größen anzeigen können
  • Aber einen hab ich noch zum Thema DATASTORE: Es sollte was geben "langzeitsores" (aka Archive) direkt daran anzuflanschen, die dann selbst bestimmen wie oft und wohin (RRD,SQL...)
  • Statt DMX/CUL/...-Konnektor kam dann die Idee vielleicht besser einen universellen UDP/TCP-Konnektor zu machen, den Inhalt dann in einem "Baustein" o.ä. parsen
  • TCP/UDP,DMX,CU*,etc-Konnektoren sollten definitiv mehrfach instanzierbar sein.
  • i18n - alles womit der Anwender in Kontakt kommt sollte von Anfang an i18n-fähig sein

Externe Links

Implementierungs-Details

Configfile format

[WireGate]
user=user
errorlog=pathtoerrorlogfile
logfile=standardlogfile
loglevel=info

[CometServer]
connector=comet_server
port=4495

[CometServer2]
connector=comet_server
port=4496

[KNX]
connector=knx_connector
url=ip:127.0.0.1
loglevel=debug

[OWFS]
connector=owfs_connector
[datastore]
logfile=pathtoDSlogfile
loglevel=debug


LogikEngine reloaded

Zur Zeit gibt es eine Diskussion über eine "vereinheitlichte" LogikEngine, die mehrere bereits existierende Ansätze unter einen Hut bringt und so Entwicklerressourcen bündeln kann.

Mögliche Ansätze dabei sind:

  • PyWireGate der hier beschriebene Ansatz von NilsS
  • GrAF ein Ansatz von Christian Mayer