mirror of
https://github.com/naruxde/revpimodio2.git
synced 2025-11-08 13:53:53 +01:00
new project started
This commit is contained in:
19
.hgignore
Normal file
19
.hgignore
Normal file
@@ -0,0 +1,19 @@
|
||||
glob:.eric6project
|
||||
glob:_eric6project
|
||||
glob:.eric5project
|
||||
glob:_eric5project
|
||||
glob:.eric4project
|
||||
glob:_eric4project
|
||||
glob:.ropeproject
|
||||
glob:_ropeproject
|
||||
glob:.directory
|
||||
glob:**.pyc
|
||||
glob:**.pyo
|
||||
glob:**.orig
|
||||
glob:**.bak
|
||||
glob:**.rej
|
||||
glob:**~
|
||||
glob:cur
|
||||
glob:tmp
|
||||
glob:__pycache__
|
||||
glob:**.DS_Store
|
||||
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
@@ -0,0 +1,2 @@
|
||||
global-exclude test/*
|
||||
global-exclude *.pyc
|
||||
336
revpimodio2.e4p
Normal file
336
revpimodio2.e4p
Normal file
@@ -0,0 +1,336 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE Project SYSTEM "Project-5.1.dtd">
|
||||
<!-- eric project file for project revpimodio2 -->
|
||||
<!-- Saved: 2017-08-13, 12:05:22 -->
|
||||
<!-- Copyright (C) 2017 Sven Sager, akira@narux.de -->
|
||||
<Project version="5.1">
|
||||
<Language>en_US</Language>
|
||||
<Hash>7ea159534ad3516e9069331120048abf9b00151e</Hash>
|
||||
<ProgLanguage mixed="0">Python3</ProgLanguage>
|
||||
<ProjectType>Console</ProjectType>
|
||||
<Description></Description>
|
||||
<Version>2.0.0</Version>
|
||||
<Author>Sven Sager</Author>
|
||||
<Email>akira@narux.de</Email>
|
||||
<Eol index="-1"/>
|
||||
<Sources>
|
||||
<Source>setup.py</Source>
|
||||
<Source>revpimodio2/modio.py</Source>
|
||||
<Source>revpimodio2/summary.py</Source>
|
||||
<Source>revpimodio2/app.py</Source>
|
||||
<Source>revpimodio2/io.py</Source>
|
||||
<Source>revpimodio2/__init__.py</Source>
|
||||
<Source>revpimodio2/device.py</Source>
|
||||
<Source>revpimodio2/helper.py</Source>
|
||||
</Sources>
|
||||
<Forms/>
|
||||
<Translations/>
|
||||
<Resources/>
|
||||
<Interfaces/>
|
||||
<Others>
|
||||
<Other>doc</Other>
|
||||
<Other>.hgignore</Other>
|
||||
</Others>
|
||||
<Vcs>
|
||||
<VcsType>Mercurial</VcsType>
|
||||
<VcsOptions>
|
||||
<dict>
|
||||
<key>
|
||||
<string>add</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>checkout</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>commit</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>diff</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>export</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>global</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>history</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>log</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>remove</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>status</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>tag</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>update</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
</dict>
|
||||
</VcsOptions>
|
||||
<VcsOtherData>
|
||||
<dict/>
|
||||
</VcsOtherData>
|
||||
</Vcs>
|
||||
<FiletypeAssociations>
|
||||
<FiletypeAssociation pattern="*.idl" type="INTERFACES"/>
|
||||
<FiletypeAssociation pattern="*.py" type="SOURCES"/>
|
||||
<FiletypeAssociation pattern="*.py3" type="SOURCES"/>
|
||||
<FiletypeAssociation pattern="*.pyw" type="SOURCES"/>
|
||||
<FiletypeAssociation pattern="*.pyw3" type="SOURCES"/>
|
||||
</FiletypeAssociations>
|
||||
<Documentation>
|
||||
<DocumentationParams>
|
||||
<dict>
|
||||
<key>
|
||||
<string>ERIC4API</string>
|
||||
</key>
|
||||
<value>
|
||||
<dict>
|
||||
<key>
|
||||
<string>ignoreDirectories</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string>deb</string>
|
||||
<string>dist</string>
|
||||
<string>doc</string>
|
||||
<string>test</string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>ignoreFilePatterns</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string>setup.py</string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>languages</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string>Python3</string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>outputFile</string>
|
||||
</key>
|
||||
<value>
|
||||
<string>eric-revpimodio.api</string>
|
||||
</value>
|
||||
<key>
|
||||
<string>useRecursion</string>
|
||||
</key>
|
||||
<value>
|
||||
<bool>True</bool>
|
||||
</value>
|
||||
</dict>
|
||||
</value>
|
||||
<key>
|
||||
<string>ERIC4DOC</string>
|
||||
</key>
|
||||
<value>
|
||||
<dict>
|
||||
<key>
|
||||
<string>ignoreDirectories</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string>deb</string>
|
||||
<string>dist</string>
|
||||
<string>doc</string>
|
||||
<string>test</string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>ignoreFilePatterns</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string>setup.py</string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>noindex</string>
|
||||
</key>
|
||||
<value>
|
||||
<bool>True</bool>
|
||||
</value>
|
||||
<key>
|
||||
<string>outputDirectory</string>
|
||||
</key>
|
||||
<value>
|
||||
<string>doc</string>
|
||||
</value>
|
||||
<key>
|
||||
<string>qtHelpEnabled</string>
|
||||
</key>
|
||||
<value>
|
||||
<bool>False</bool>
|
||||
</value>
|
||||
<key>
|
||||
<string>sourceExtensions</string>
|
||||
</key>
|
||||
<value>
|
||||
<list>
|
||||
<string></string>
|
||||
</list>
|
||||
</value>
|
||||
<key>
|
||||
<string>useRecursion</string>
|
||||
</key>
|
||||
<value>
|
||||
<bool>True</bool>
|
||||
</value>
|
||||
</dict>
|
||||
</value>
|
||||
</dict>
|
||||
</DocumentationParams>
|
||||
</Documentation>
|
||||
<Checkers>
|
||||
<CheckersParams>
|
||||
<dict>
|
||||
<key>
|
||||
<string>Pep8Checker</string>
|
||||
</key>
|
||||
<value>
|
||||
<dict>
|
||||
<key>
|
||||
<string>DocstringType</string>
|
||||
</key>
|
||||
<value>
|
||||
<string>pep257</string>
|
||||
</value>
|
||||
<key>
|
||||
<string>ExcludeFiles</string>
|
||||
</key>
|
||||
<value>
|
||||
<string></string>
|
||||
</value>
|
||||
<key>
|
||||
<string>ExcludeMessages</string>
|
||||
</key>
|
||||
<value>
|
||||
<string>E123,E226,E24</string>
|
||||
</value>
|
||||
<key>
|
||||
<string>FixCodes</string>
|
||||
</key>
|
||||
<value>
|
||||
<string></string>
|
||||
</value>
|
||||
<key>
|
||||
<string>FixIssues</string>
|
||||
</key>
|
||||
<value>
|
||||
<bool>False</bool>
|
||||
</value>
|
||||
<key>
|
||||
<string>HangClosing</string>
|
||||
</key>
|
||||
<value>
|
||||
<bool>False</bool>
|
||||
</value>
|
||||
<key>
|
||||
<string>IncludeMessages</string>
|
||||
</key>
|
||||
<value>
|
||||
<string></string>
|
||||
</value>
|
||||
<key>
|
||||
<string>MaxLineLength</string>
|
||||
</key>
|
||||
<value>
|
||||
<int>79</int>
|
||||
</value>
|
||||
<key>
|
||||
<string>NoFixCodes</string>
|
||||
</key>
|
||||
<value>
|
||||
<string>E501</string>
|
||||
</value>
|
||||
<key>
|
||||
<string>RepeatMessages</string>
|
||||
</key>
|
||||
<value>
|
||||
<bool>True</bool>
|
||||
</value>
|
||||
<key>
|
||||
<string>ShowIgnored</string>
|
||||
</key>
|
||||
<value>
|
||||
<bool>False</bool>
|
||||
</value>
|
||||
</dict>
|
||||
</value>
|
||||
</dict>
|
||||
</CheckersParams>
|
||||
</Checkers>
|
||||
</Project>
|
||||
34
revpimodio2/__init__.py
Normal file
34
revpimodio2/__init__.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# python3-RevPiModIO
|
||||
#
|
||||
# Webpage: https://revpimodio.org/
|
||||
# (c) Sven Sager, License: LGPLv3
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Stellt alle Klassen fuer den RevolutionPi zur Verfuegung.
|
||||
|
||||
Stellt Klassen fuer die einfache Verwendung des Revolution Pis der
|
||||
Kunbus GmbH (https://revolution.kunbus.de/) zur Verfuegung. Alle I/Os werden
|
||||
aus der piCtory Konfiguration eingelesen und mit deren Namen direkt zugreifbar
|
||||
gemacht. Fuer Gateways sind eigene IOs ueber mehrere Bytes konfigurierbar
|
||||
Mit den definierten Namen greift man direkt auf die gewuenschten Daten zu.
|
||||
Auf alle IOs kann der Benutzer Funktionen als Events registrieren. Diese
|
||||
fuehrt das Modul bei Datenaenderung aus.
|
||||
|
||||
"""
|
||||
import warnings
|
||||
from .modio import *
|
||||
|
||||
__all__ = ["RevPiModIO", "RevPiModIOSelected", "RevPiModIODriver"]
|
||||
__author__ = "Sven Sager <akira@revpimodio.org>"
|
||||
__version__ = "2.0.0"
|
||||
|
||||
# Global package values
|
||||
OFF = 0
|
||||
GREEN = 1
|
||||
RED = 2
|
||||
RISING = 31
|
||||
FALLING = 32
|
||||
BOTH = 33
|
||||
|
||||
warnings.simplefilter(action="always")
|
||||
22
revpimodio2/app.py
Normal file
22
revpimodio2/app.py
Normal file
@@ -0,0 +1,22 @@
|
||||
#
|
||||
# python3-RevPiModIO
|
||||
#
|
||||
# Webpage: https://revpimodio.org/
|
||||
# (c) Sven Sager, License: LGPLv3
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Bildet die App Sektion von piCtory ab."""
|
||||
|
||||
|
||||
class App(object):
|
||||
|
||||
"""Bildet die App Sektion der config.rsc ab."""
|
||||
|
||||
def __init__(self, app):
|
||||
"""Instantiiert die App-Klasse.
|
||||
@param app piCtory Appinformationen"""
|
||||
self.name = app["name"]
|
||||
self.version = app["version"]
|
||||
self.language = app["language"]
|
||||
# TODO: Layout untersuchen und anders abbilden
|
||||
self.layout = app["layout"]
|
||||
1170
revpimodio2/device.py
Normal file
1170
revpimodio2/device.py
Normal file
File diff suppressed because it is too large
Load Diff
355
revpimodio2/helper.py
Normal file
355
revpimodio2/helper.py
Normal file
@@ -0,0 +1,355 @@
|
||||
#
|
||||
# python3-RevPiModIO
|
||||
#
|
||||
# Webpage: https://revpimodio.org/
|
||||
# (c) Sven Sager, License: LGPLv3
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
import warnings
|
||||
from threading import Event, Lock, Thread
|
||||
from timeit import default_timer
|
||||
|
||||
|
||||
class EventCallback(Thread):
|
||||
|
||||
"""Thread fuer das interne Aufrufen von Event-Funktionen.
|
||||
|
||||
Der Eventfunktion, welche dieser Thread aufruft, wird der Thread selber
|
||||
als Parameter uebergeben. Darauf muss bei der definition der Funktion
|
||||
geachtet werden z.B. "def event(th):". Bei umfangreichen Funktionen kann
|
||||
dieser ausgewertet werden um z.B. doppeltes Starten zu verhindern.
|
||||
Ueber EventCallback.ioname kann der Name des IO-Objekts abgerufen werden,
|
||||
welches das Event ausgeloest hast. EventCallback.iovalue gibt den Wert des
|
||||
IO-Objekts zum Ausloesezeitpunkt zurueck.
|
||||
Der Thread stellt das EventCallback.exit Event als Abbruchbedingung fuer
|
||||
die aufgerufene Funktion zur Verfuegung.
|
||||
Durch Aufruf der Funktion EventCallback.stop() wird das exit-Event gesetzt
|
||||
und kann bei Schleifen zum Abbrechen verwendet werden.
|
||||
Mit dem .exit() Event auch eine Wartefunktion realisiert
|
||||
werden: "th.exit.wait(0.5)" - Wartet 500ms oder bricht sofort ab, wenn
|
||||
fuer den Thread .stop() aufgerufen wird.
|
||||
|
||||
while not th.exit.is_set():
|
||||
# IO-Arbeiten
|
||||
th.exit.wait(0.5)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, func, name, value):
|
||||
"""Init EventCallback class.
|
||||
|
||||
@param func Funktion die beim Start aufgerufen werden soll
|
||||
@param name IO-Name
|
||||
@param value IO-Value zum Zeitpunkt des Events
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
self.daemon = True
|
||||
self.exit = Event()
|
||||
self.func = func
|
||||
self.ioname = name
|
||||
self.iovalue = value
|
||||
|
||||
def run(self):
|
||||
"""Ruft die registrierte Funktion auf."""
|
||||
self.func(self)
|
||||
|
||||
def stop(self):
|
||||
"""Setzt das exit-Event mit dem die Funktion beendet werden kann."""
|
||||
self.exit.set()
|
||||
|
||||
|
||||
class Cycletools():
|
||||
|
||||
"""Werkzeugkasten fuer Cycleloop-Funktion.
|
||||
|
||||
Diese Klasse enthaelt Werkzeuge fuer Zyklusfunktionen, wie Taktmerker
|
||||
und Flankenmerker.
|
||||
Zu beachten ist, dass die Flankenmerker beim ersten Zyklus alle den Wert
|
||||
True haben! Ueber den Merker Cycletools.first kann ermittelt werden,
|
||||
ob es sich um den ersten Zyklus handelt.
|
||||
|
||||
Taktmerker flag1c, flag5c, flag10c, usw. haben den als Zahl angegebenen
|
||||
Wert an Zyklen jeweils False und True.
|
||||
Beispiel: flag5c hat 5 Zyklen den Wert False und in den naechsten 5 Zyklen
|
||||
den Wert True.
|
||||
|
||||
Flankenmerker flank5c, flank10c, usw. haben immer im, als Zahl angebenen
|
||||
Zyklus fuer einen Zyklusdurchlauf den Wert True, sonst False.
|
||||
Beispiel: flank5c hat immer alle 5 Zyklen den Wert True.
|
||||
|
||||
Diese Merker koennen z.B. verwendet werden um, an Outputs angeschlossene,
|
||||
Lampen synchron blinken zu lassen.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Init Cycletools class."""
|
||||
self.__cycle = 0
|
||||
self.__ucycle = 0
|
||||
self.__dict_ton = {}
|
||||
self.__dict_tof = {}
|
||||
self.__dict_tp = {}
|
||||
|
||||
# Taktmerker
|
||||
self.first = True
|
||||
self.flag1c = False
|
||||
self.flag5c = False
|
||||
self.flag10c = False
|
||||
self.flag15c = False
|
||||
self.flag20c = False
|
||||
|
||||
# Flankenmerker
|
||||
self.flank5c = True
|
||||
self.flank10c = True
|
||||
self.flank15c = True
|
||||
self.flank20c = True
|
||||
|
||||
def _docycle(self):
|
||||
"""Zyklusarbeiten."""
|
||||
# Einschaltverzoegerung
|
||||
for tof in self.__dict_tof:
|
||||
if self.__dict_tof[tof] > 0:
|
||||
self.__dict_tof[tof] -= 1
|
||||
|
||||
# Ausschaltverzoegerung
|
||||
for ton in self.__dict_ton:
|
||||
if self.__dict_ton[ton][1]:
|
||||
if self.__dict_ton[ton][0] > 0:
|
||||
self.__dict_ton[ton][0] -= 1
|
||||
self.__dict_ton[ton][1] = False
|
||||
else:
|
||||
self.__dict_ton[ton][0] = -1
|
||||
|
||||
# Impuls
|
||||
for tp in self.__dict_tp:
|
||||
if self.__dict_tp[tp][1]:
|
||||
if self.__dict_tp[tp][0] > 0:
|
||||
self.__dict_tp[tp][0] -= 1
|
||||
else:
|
||||
self.__dict_tp[tp][1] = False
|
||||
else:
|
||||
self.__dict_tp[tp][0] = -1
|
||||
|
||||
# Flankenmerker
|
||||
self.flank5c = False
|
||||
self.flank10c = False
|
||||
self.flank15c = False
|
||||
self.flank20c = False
|
||||
|
||||
# Logische Flags
|
||||
self.first = False
|
||||
self.flag1c = not self.flag1c
|
||||
|
||||
# Berechnete Flags
|
||||
self.__cycle += 1
|
||||
if self.__cycle == 5:
|
||||
self.__ucycle += 1
|
||||
if self.__ucycle == 3:
|
||||
self.flank15c = True
|
||||
self.flag15c = not self.flag15c
|
||||
self.__ucycle = 0
|
||||
if self.flag5c:
|
||||
if self.flag10c:
|
||||
self.flank20c = True
|
||||
self.flag20c = not self.flag20c
|
||||
self.flank10c = True
|
||||
self.flag10c = not self.flag10c
|
||||
self.flank5c = True
|
||||
self.flag5c = not self.flag5c
|
||||
self.__cycle = 0
|
||||
|
||||
def get_tofc(self, name):
|
||||
"""Wert der Ausschaltverzoegerung.
|
||||
@param name Eindeutiger Name des Timers
|
||||
@return Wert der Ausschaltverzoegerung"""
|
||||
return self.__dict_tof.get(name, 0) > 0
|
||||
|
||||
def set_tofc(self, name, cycles):
|
||||
"""Startet bei Aufruf einen ausschaltverzoegerten Timer.
|
||||
|
||||
@param name Eindeutiger Name fuer Zugriff auf Timer
|
||||
@param cycles Zyklusanzahl, der Verzoegerung wenn nicht neu gestartet
|
||||
|
||||
"""
|
||||
self.__dict_tof[name] = cycles
|
||||
|
||||
def get_tonc(self, name):
|
||||
"""Einschaltverzoegerung.
|
||||
@param name Eindeutiger Name des Timers
|
||||
@return Wert der Einschaltverzoegerung"""
|
||||
return self.__dict_ton.get(name, [-1])[0] == 0
|
||||
|
||||
def set_tonc(self, name, cycles):
|
||||
"""Startet einen einschaltverzoegerten Timer.
|
||||
|
||||
@param name Eindeutiger Name fuer Zugriff auf Timer
|
||||
@param cycles Zyklusanzahl, der Verzoegerung wenn neu gestartet
|
||||
|
||||
"""
|
||||
if self.__dict_ton.get(name, [-1])[0] == -1:
|
||||
self.__dict_ton[name] = [cycles, True]
|
||||
else:
|
||||
self.__dict_ton[name][1] = True
|
||||
|
||||
def get_tpc(self, name):
|
||||
"""Impulstimer.
|
||||
@param name Eindeutiger Name des Timers
|
||||
@return Wert der des Impulses"""
|
||||
return self.__dict_tp.get(name, [-1])[0] > 0
|
||||
|
||||
def set_tpc(self, name, cycles):
|
||||
"""Startet einen Impuls Timer.
|
||||
|
||||
@param name Eindeutiger Name fuer Zugriff auf Timer
|
||||
@param cycles Zyklusanzahl, die der Impuls anstehen soll
|
||||
|
||||
"""
|
||||
if self.__dict_tp.get(name, [-1])[0] == -1:
|
||||
self.__dict_tp[name] = [cycles, True]
|
||||
else:
|
||||
self.__dict_tp[name][1] = True
|
||||
|
||||
|
||||
class ProcimgWriter(Thread):
|
||||
|
||||
"""Klasse fuer Synchroniseriungs-Thread.
|
||||
|
||||
Diese Klasse wird als Thread gestartet, wenn das Prozessabbild zyklisch
|
||||
synchronisiert werden soll. Diese Funktion wird hauptsaechlich fuer das
|
||||
Event-Handling verwendet.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, parentmodio):
|
||||
"""Init ProcimgWriter class.
|
||||
@param parentmodio Parent Object"""
|
||||
super().__init__()
|
||||
self._adjwait = 0
|
||||
self._ioerror = 0
|
||||
self._modio = parentmodio
|
||||
self._refresh = 0.05
|
||||
self._work = Event()
|
||||
|
||||
self.daemon = True
|
||||
self.lck_refresh = Lock()
|
||||
self.maxioerrors = 0
|
||||
self.newdata = Event()
|
||||
|
||||
def _gotioerror(self):
|
||||
"""IOError Verwaltung fuer auto_refresh."""
|
||||
self._ioerror += 1
|
||||
if self.maxioerrors != 0 and self._ioerror >= self.maxioerrors:
|
||||
raise RuntimeError(
|
||||
"reach max io error count {} on process image".format(
|
||||
self.maxioerrors
|
||||
)
|
||||
)
|
||||
warnings.warn(
|
||||
"count {} io errors on process image".format(self._ioerror),
|
||||
RuntimeWarning
|
||||
)
|
||||
|
||||
def get_refresh(self):
|
||||
"""Gibt Zykluszeit zurueck.
|
||||
@return int() Zykluszeit in Millisekunden"""
|
||||
return int(self._refresh * 1000)
|
||||
|
||||
def run(self):
|
||||
"""Startet die automatische Prozessabbildsynchronisierung."""
|
||||
fh = self._modio._create_myfh()
|
||||
self._adjwait = self._refresh
|
||||
while not self._work.is_set():
|
||||
ot = default_timer()
|
||||
|
||||
# Lockobjekt holen und Fehler werfen, wenn nicht schnell genug
|
||||
if not self.lck_refresh.acquire(timeout=self._adjwait):
|
||||
warnings.warn(
|
||||
"cycle time of {} ms exceeded on lock".format(
|
||||
int(self._refresh * 1000)
|
||||
),
|
||||
RuntimeWarning
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
fh.seek(0)
|
||||
bytesbuff = bytearray(fh.read(self._modio._length))
|
||||
except IOError:
|
||||
self._gotioerror()
|
||||
self.lck_refresh.release()
|
||||
self._work.wait(self._adjwait)
|
||||
continue
|
||||
|
||||
if self._modio._monitoring:
|
||||
# Inputs und Outputs in Puffer
|
||||
for dev in self._modio._lst_refresh:
|
||||
dev._filelock.acquire()
|
||||
dev._ba_devdata[:] = bytesbuff[dev.slc_devoff]
|
||||
dev._filelock.release()
|
||||
else:
|
||||
# Inputs in Puffer, Outputs in Prozessabbild
|
||||
ioerr = False
|
||||
for dev in self._modio._lst_refresh:
|
||||
dev._filelock.acquire()
|
||||
dev._ba_devdata[dev.slc_inp] = bytesbuff[dev.slc_inpoff]
|
||||
try:
|
||||
fh.seek(dev.slc_outoff.start)
|
||||
fh.write(dev._ba_devdata[dev.slc_out])
|
||||
except IOError:
|
||||
ioerr = True
|
||||
finally:
|
||||
dev._filelock.release()
|
||||
|
||||
if self._modio._buffedwrite:
|
||||
try:
|
||||
fh.flush()
|
||||
except IOError:
|
||||
ioerr = True
|
||||
|
||||
if ioerr:
|
||||
self._gotioerror()
|
||||
self.lck_refresh.release()
|
||||
self._work.wait(self._adjwait)
|
||||
continue
|
||||
|
||||
self.lck_refresh.release()
|
||||
|
||||
# Alle aufwecken
|
||||
self.newdata.set()
|
||||
self._work.wait(self._adjwait)
|
||||
|
||||
# Wartezeit anpassen um echte self._refresh zu erreichen
|
||||
if default_timer() - ot >= self._refresh:
|
||||
self._adjwait -= 0.001
|
||||
if self._adjwait < 0:
|
||||
warnings.warn(
|
||||
"cycle time of {} ms exceeded".format(
|
||||
int(self._refresh * 1000)
|
||||
),
|
||||
RuntimeWarning
|
||||
)
|
||||
self._adjwait = 0
|
||||
else:
|
||||
self._adjwait += 0.001
|
||||
|
||||
# Alle am Ende erneut aufwecken
|
||||
self.newdata.set()
|
||||
fh.close()
|
||||
|
||||
def stop(self):
|
||||
"""Beendet die automatische Prozessabbildsynchronisierung."""
|
||||
self._work.set()
|
||||
|
||||
def set_refresh(self, value):
|
||||
"""Setzt die Zykluszeit in Millisekunden.
|
||||
@param value int() Millisekunden"""
|
||||
if value >= 10 and value < 2000:
|
||||
self._refresh = value / 1000
|
||||
self._adjwait = self._refresh
|
||||
else:
|
||||
raise ValueError(
|
||||
"refresh time must be 10 to 2000 milliseconds"
|
||||
)
|
||||
|
||||
refresh = property(get_refresh, set_refresh)
|
||||
631
revpimodio2/io.py
Normal file
631
revpimodio2/io.py
Normal file
@@ -0,0 +1,631 @@
|
||||
#
|
||||
# python3-RevPiModIO
|
||||
#
|
||||
# Webpage: https://revpimodio.org/
|
||||
# (c) Sven Sager, License: LGPLv3
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
import struct
|
||||
from .__init__ import RISING, FALLING, BOTH
|
||||
from .device import Gateway
|
||||
from threading import Event
|
||||
|
||||
|
||||
class IOType(object):
|
||||
|
||||
"""IO Typen."""
|
||||
|
||||
INP = 300
|
||||
OUT = 301
|
||||
MEM = 302
|
||||
|
||||
|
||||
class IOList(object):
|
||||
|
||||
"""Basisklasse fuer direkten Zugriff auf IO Objekte."""
|
||||
|
||||
def __init__(self):
|
||||
"""Init IOList clacc."""
|
||||
self.__dict_iobyte = {k: [] for k in range(4096)}
|
||||
|
||||
def __contains__(self, key):
|
||||
"""Prueft ob IO existiert.
|
||||
@param key IO-Name str()
|
||||
@return True, wenn IO vorhanden"""
|
||||
if type(key) == int:
|
||||
return key in self.__dict_iobyte \
|
||||
and len(self.__dict_iobyte[key]) > 0
|
||||
else:
|
||||
return hasattr(self, key)
|
||||
|
||||
def __delattr__(self, key):
|
||||
"""Entfernt angegebenen IO.
|
||||
@param key IO zum entfernen"""
|
||||
# TODO: Prüfen ob auch Bit sein kann
|
||||
dev = getattr(self, key)
|
||||
self.__dict_iobyte[dev.address].remove(dev)
|
||||
object.__delattr__(self, key)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Ruft angegebenen IO ab.
|
||||
@param key IO Name oder Byte
|
||||
@return IO Object"""
|
||||
if type(key) == int:
|
||||
if key in self.__dict_iobyte:
|
||||
return self.__dict_iobyte[key]
|
||||
else:
|
||||
raise KeyError("byte '{}' does not exist".format(key))
|
||||
else:
|
||||
return getattr(self, key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Setzt IO Wert.
|
||||
@param key IO Name oder Byte
|
||||
@param value Wert, auf den der IO gesetzt wird"""
|
||||
if type(key) == int:
|
||||
if key in self.__dict_iobyte:
|
||||
if len(self.__dict_iobyte[key]) == 1:
|
||||
self.__dict_iobyte[key][0].value = value
|
||||
elif len(self.__dict_iobyte[key]) == 0:
|
||||
raise KeyError("byte '{}' contains no input".format(key))
|
||||
else:
|
||||
raise KeyError(
|
||||
"byte '{}' contains more than one bit-input"
|
||||
"".format(key)
|
||||
)
|
||||
else:
|
||||
raise KeyError("byte '{}' does not exist".format(key))
|
||||
else:
|
||||
getattr(self, key).value = value
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
"""Setzt IO Wert.
|
||||
@param key IO Name oder Byte
|
||||
@param value Wert, auf den der IO gesetzt wird"""
|
||||
if issubclass(type(value), IOBase):
|
||||
if hasattr(self, key):
|
||||
raise AttributeError(
|
||||
"attribute {} already exists - can not set io".format(key)
|
||||
)
|
||||
object.__setattr__(self, key, value)
|
||||
|
||||
# Bytedict erstellen für Adresszugriff
|
||||
if value._bitaddress < 0:
|
||||
self.__dict_iobyte[value.address].append(value)
|
||||
else:
|
||||
if len(self.__dict_iobyte[value.address]) != 8:
|
||||
# "schnell" 8 Einträge erstellen da es BIT IOs sind
|
||||
self.__dict_iobyte[value.address] += [
|
||||
None, None, None, None, None, None, None, None
|
||||
]
|
||||
self.__dict_iobyte[value.address][value._bitaddress] = value
|
||||
|
||||
elif key == "_IOList__dict_iobyte":
|
||||
object.__setattr__(self, key, value)
|
||||
|
||||
else:
|
||||
getattr(self, key).value = value
|
||||
|
||||
def _testme(self):
|
||||
# NOTE: Nur Debugging
|
||||
for x in self.__dict_iobyte:
|
||||
if len(self.__dict_iobyte[x]) > 0:
|
||||
print(x, self.__dict_iobyte[x])
|
||||
|
||||
def reg_inp(self, name, frm, **kwargs):
|
||||
"""Registriert einen neuen Input an Adresse von Diesem.
|
||||
|
||||
@param name Name des neuen Inputs
|
||||
@param frm struct() formatierung (1 Zeichen)
|
||||
@param kwargs Weitere Parameter:
|
||||
- bmk: Bezeichnung fuer Input
|
||||
- bit: Registriert Input als bool() am angegebenen Bit im Byte
|
||||
- byteorder: Byteorder fuer den Input, Standardwert=little
|
||||
- defaultvalue: Standardwert fuer Input, Standard ist 0
|
||||
- event: Funktion fuer Eventhandling registrieren
|
||||
- as_thread: Fuehrt die event-Funktion als RevPiCallback-Thread aus
|
||||
- edge: event-Ausfuehren bei RISING, FALLING or BOTH Wertaenderung
|
||||
@see <a target="_blank"
|
||||
href="https://docs.python.org/3/library/struct.html#format-characters"
|
||||
>Python3 struct()</a>
|
||||
|
||||
"""
|
||||
if not issubclass(self._parentdevice, Gateway):
|
||||
raise RuntimeError(
|
||||
"this function can be used on gatway or virtual devices only"
|
||||
)
|
||||
|
||||
self._create_io(name, startinp, frm, IOType.INP, **kwargs)
|
||||
|
||||
# Optional Event eintragen
|
||||
reg_event = kwargs.get("event", None)
|
||||
if reg_event is not None:
|
||||
as_thread = kwargs.get("as_thread", False)
|
||||
edge = kwargs.get("edge", None)
|
||||
self.reg_event(name, reg_event, as_thread=as_thread, edge=edge)
|
||||
|
||||
def reg_out(self, name, startout, frm, **kwargs):
|
||||
"""Registriert einen neuen Output.
|
||||
|
||||
@param name Name des neuen Outputs
|
||||
@param startout Outputname ab dem eingefuegt wird
|
||||
@param frm struct() formatierung (1 Zeichen)
|
||||
@param kwargs Weitere Parameter:
|
||||
- bmk: Bezeichnung fuer Output
|
||||
- bit: Registriert Outputs als bool() am angegebenen Bit im Byte
|
||||
- byteorder: Byteorder fuer den Output, Standardwert=little
|
||||
- defaultvalue: Standardwert fuer Output, Standard ist 0
|
||||
- event: Funktion fuer Eventhandling registrieren
|
||||
- as_thread: Fuehrt die event-Funktion als RevPiCallback-Thread aus
|
||||
- edge: event-Ausfuehren bei RISING, FALLING or BOTH Wertaenderung
|
||||
@see <a target="_blank"
|
||||
href="https://docs.python.org/3/library/struct.html#format-characters"
|
||||
>Python3 struct()</a>
|
||||
|
||||
"""
|
||||
if not issubclass(self._parentdevice, Gateway):
|
||||
raise RuntimeError(
|
||||
"this function can be used on gatway or virtual devices only"
|
||||
)
|
||||
|
||||
self._create_io(name, startout, frm, IOType.OUT, **kwargs)
|
||||
|
||||
# Optional Event eintragen
|
||||
reg_event = kwargs.get("event", None)
|
||||
if reg_event is not None:
|
||||
as_thread = kwargs.get("as_thread", False)
|
||||
edge = kwargs.get("edge", None)
|
||||
self.reg_event(name, reg_event, as_thread=as_thread, edge=edge)
|
||||
|
||||
|
||||
class IOBase(object):
|
||||
|
||||
"""Basisklasse fuer alle IO-Objekte.
|
||||
|
||||
Die Basisfunktionalitaet ermoeglicht das Lesen und Schreiben der Werte
|
||||
als bytes() oder bool(). Dies entscheidet sich bei der Instantiierung.
|
||||
Wenn eine Bittadresse angegeben wird, werden bool()-Werte erwartet
|
||||
und zurueckgegeben, ansonsten bytes().
|
||||
|
||||
Diese Klasse dient als Basis fuer andere IO-Klassen mit denen die Werte
|
||||
auch als int() verwendet werden koennen.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, parentdevice, valuelist, iotype, byteorder):
|
||||
"""Instantiierung der IOBase()-Klasse.
|
||||
|
||||
@param parentdevice Parentdevice auf dem der IO liegt
|
||||
@param valuelist Datenliste fuer Instantiierung
|
||||
@param iotype IOType() Wert
|
||||
@param byteorder Byteorder 'little' / 'big' fuer int() Berechnung
|
||||
|
||||
"""
|
||||
self._parentdevice = parentdevice
|
||||
|
||||
# Bitadressen auf Bytes aufbrechen und umrechnen
|
||||
self._bitaddress = -1 if valuelist[7] == "" else int(valuelist[7]) % 8
|
||||
|
||||
# Längenberechnung
|
||||
self._bitlength = int(valuelist[2])
|
||||
self._length = 1 if self._bitaddress == 0 else int(self._bitlength / 8)
|
||||
|
||||
self._byteorder = byteorder
|
||||
self._iotype = iotype
|
||||
self._name = valuelist[0]
|
||||
self._signed = False
|
||||
self.bmk = valuelist[6]
|
||||
|
||||
int_startaddress = int(valuelist[3])
|
||||
if self._bitaddress == -1:
|
||||
self.slc_address = slice(
|
||||
int_startaddress, int_startaddress + self._length
|
||||
)
|
||||
# Defaultvalue aus Zahl in Bytes umrechnen
|
||||
if str(valuelist[1]).isnumeric():
|
||||
self.defaultvalue = int(valuelist[1]).to_bytes(
|
||||
self._length, byteorder=self._byteorder
|
||||
)
|
||||
else:
|
||||
# Defaultvalue direkt von bytes übernehmen
|
||||
if type(valuelist[1]) == bytes:
|
||||
if len(valuelist[1]) != self._length:
|
||||
raise ValueError(
|
||||
"given bytes for default value must have a length "
|
||||
"of {}".format(self._length)
|
||||
)
|
||||
else:
|
||||
self.defaultvalue = valuelist[1]
|
||||
else:
|
||||
self.defaultvalue = bytes(self._length)
|
||||
|
||||
else:
|
||||
# Höhere Bits als 7 auf nächste Bytes umbrechen
|
||||
int_startaddress += int((int(valuelist[7]) % 16) / 8)
|
||||
self.slc_address = slice(
|
||||
int_startaddress, int_startaddress + 1
|
||||
)
|
||||
self.defaultvalue = bool(int(valuelist[1]))
|
||||
|
||||
def __bool__(self):
|
||||
"""bool()-wert der Klasse.
|
||||
@return IO-Wert als bool(). Nur False wenn False oder 0 sonst True"""
|
||||
return bool(self.get_value())
|
||||
|
||||
def __bytes__(self):
|
||||
"""bytes()-wert der Klasse.
|
||||
@return IO-Wert als bytes()"""
|
||||
if self._bitaddress >= 0:
|
||||
int_byte = int.from_bytes(
|
||||
self._parentdevice._ba_devdata[self.slc_address],
|
||||
byteorder=self._byteorder
|
||||
)
|
||||
return b'\x01' if bool(int_byte & 1 << self._bitaddress) \
|
||||
else b'\x00'
|
||||
else:
|
||||
return bytes(self._parentdevice._ba_devdata[self.slc_address])
|
||||
|
||||
def __str__(self):
|
||||
"""str()-wert der Klasse.
|
||||
@return Namen des IOs"""
|
||||
return self._name
|
||||
|
||||
def _get_byteorder(self):
|
||||
"""Gibt konfigurierte Byteorder zurueck.
|
||||
@return str() Byteorder"""
|
||||
return self._byteorder
|
||||
|
||||
def get_address(self):
|
||||
"""Gibt die absolute Byteadresse im Prozessabbild zurueck.
|
||||
@return Absolute Byteadresse"""
|
||||
return self._parentdevice.offset + self.slc_address.start
|
||||
|
||||
def get_length(self):
|
||||
"""Gibt die Bytelaenge des IO zurueck.
|
||||
@return Bytelaenge des IO"""
|
||||
return self._length
|
||||
|
||||
def get_name(self):
|
||||
"""Gibt den Namen des IOs zurueck.
|
||||
@return IO Name"""
|
||||
return self._name
|
||||
|
||||
def get_value(self):
|
||||
"""Gibt den Wert des IOs als bytes() oder bool() zurueck.
|
||||
@return IO-Wert"""
|
||||
if self._bitaddress >= 0:
|
||||
int_byte = int.from_bytes(
|
||||
self._parentdevice._ba_devdata[self.slc_address],
|
||||
byteorder=self._byteorder
|
||||
)
|
||||
return bool(int_byte & 1 << self._bitaddress)
|
||||
|
||||
else:
|
||||
return bytes(self._parentdevice._ba_devdata[self.slc_address])
|
||||
|
||||
def reg_event(self, func, edge=BOTH, as_thread=False):
|
||||
"""Registriert ein Event bei der Eventueberwachung.
|
||||
|
||||
@param func Funktion die bei Aenderung aufgerufen werden soll
|
||||
@param edge Ausfuehren bei RISING, FALLING or BOTH Wertaenderung
|
||||
@param as_thread Bei True, Funktion als EventCallback-Thread ausfuehren
|
||||
|
||||
"""
|
||||
# Prüfen ob Funktion callable ist
|
||||
if not callable(func):
|
||||
raise RuntimeError(
|
||||
"registered function '{}' ist not callable".format(func)
|
||||
)
|
||||
|
||||
if edge != BOTH and self._bitaddress < 0:
|
||||
raise AttributeError(
|
||||
"parameter 'edge' can be used with bit io objects only"
|
||||
)
|
||||
|
||||
if self not in self._parentdevice._dict_events:
|
||||
self._parentdevice._dict_events[self] = [(func, edge, as_thread)]
|
||||
else:
|
||||
# Prüfen ob Funktion schon registriert ist
|
||||
for regfunc in self._parentdevice._dict_events[self]:
|
||||
|
||||
if regfunc[0] == func and edge == BOTH:
|
||||
if self._bitaddress < 0:
|
||||
raise AttributeError(
|
||||
"io '{}' with function '{}' already in list."
|
||||
"".format(self._name, func)
|
||||
)
|
||||
else:
|
||||
raise AttributeError(
|
||||
"io '{}' with function '{}' already in list. "
|
||||
"edge 'BOTH' not allowed anymore".format(
|
||||
self._name, func
|
||||
)
|
||||
)
|
||||
elif regfunc[0] == func and regfunc[1] == edge:
|
||||
raise AttributeError(
|
||||
"io '{}' with function '{}' for given edge "
|
||||
"already in list".format(self._name, func)
|
||||
)
|
||||
else:
|
||||
self._parentdevice._dict_events[self].append(
|
||||
(func, edge, as_thread)
|
||||
)
|
||||
break
|
||||
|
||||
def set_value(self, value):
|
||||
"""Setzt den Wert des IOs mit bytes() oder bool().
|
||||
@param value IO-Wert als bytes() oder bool()"""
|
||||
if self._iotype == IOType.OUT:
|
||||
if self._bitaddress >= 0:
|
||||
# Versuchen egal welchen Typ in Bool zu konvertieren
|
||||
value = bool(value)
|
||||
|
||||
# ganzes Byte laden
|
||||
byte_buff = self._parentdevice._ba_devdata[self.slc_address]
|
||||
|
||||
# Bytes in integer umwandeln
|
||||
int_len = len(byte_buff)
|
||||
int_byte = int.from_bytes(byte_buff, byteorder=self._byteorder)
|
||||
int_bit = 1 << self._bitaddress
|
||||
|
||||
# Aktuellen Wert vergleichen und ggf. setzen
|
||||
if not bool(int_byte & int_bit) == value:
|
||||
if value:
|
||||
int_byte += int_bit
|
||||
else:
|
||||
int_byte -= int_bit
|
||||
|
||||
# Zurückschreiben wenn verändert
|
||||
self._parentdevice._ba_devdata[self.slc_address] = \
|
||||
int_byte.to_bytes(int_len, byteorder=self._byteorder)
|
||||
|
||||
else:
|
||||
if type(value) == bytes:
|
||||
if self._length == len(value):
|
||||
self._parentdevice._ba_devdata[self.slc_address] = \
|
||||
value
|
||||
else:
|
||||
raise ValueError(
|
||||
"requires a bytes() object of length {}, but"
|
||||
" {} was given".format(self._length, len(value))
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"requires a bytes() object, not {}".format(type(value))
|
||||
)
|
||||
|
||||
elif self._iotype == IOType.INP:
|
||||
raise AttributeError("can not write to input")
|
||||
elif self._iotype == IOType.MEM:
|
||||
raise AttributeError("can not write to memory")
|
||||
|
||||
def unreg_event(self, func=None, edge=None):
|
||||
"""Entfernt ein Event aus der Eventueberwachung.
|
||||
|
||||
@param func Nur Events mit angegebener Funktion
|
||||
@param edge Nur Events mit angegebener Funktion und angegebener Edge
|
||||
|
||||
"""
|
||||
if self in self._parentdevice._dict_events:
|
||||
if func is None:
|
||||
del self._parentdevice._dict_events[self]
|
||||
else:
|
||||
newlist = []
|
||||
for regfunc in self._parentdevice._dict_events[self]:
|
||||
if regfunc[0] != func or edge is not None \
|
||||
and regfunc[1] != edge:
|
||||
|
||||
newlist.append(regfunc)
|
||||
|
||||
# Wenn Funktionen übrig bleiben, diese übernehmen
|
||||
if len(newlist) > 0:
|
||||
self._parentdevice._dict_events[self] = newlist
|
||||
else:
|
||||
del self._parentdevice._dict_events[self]
|
||||
|
||||
def wait(self, edge=BOTH, exitevent=None, okvalue=None, timeout=0):
|
||||
"""Wartet auf Wertaenderung eines IOs.
|
||||
|
||||
Die Wertaenderung wird immer uerberprueft, wenn fuer Devices
|
||||
in Devicelist.auto_refresh() neue Daten gelesen wurden.
|
||||
|
||||
Bei Wertaenderung, wird das Warten mit 0 als Rueckgabewert beendet.
|
||||
|
||||
HINWEIS: Wenn ProcimgWriter() keine neuen Daten liefert, wird
|
||||
bis in die Ewigkeit gewartet (nicht bei Angabe von "timeout").
|
||||
|
||||
Wenn edge mit RISING oder FALLING angegeben wird muss diese Flanke
|
||||
ausgeloest werden. Sollte der Wert 1 sein beim Eintritt mit Flanke
|
||||
RISING, wird das Warten erst bei Aenderung von 0 auf 1 beendet.
|
||||
|
||||
Als exitevent kann ein threading.Event()-Objekt uebergeben werden,
|
||||
welches das Warten bei is_set() sofort mit 1 als Rueckgabewert
|
||||
beendet.
|
||||
|
||||
Wenn der Wert okvalue an dem IO fuer das Warten anliegt, wird
|
||||
das Warten sofort mit -1 als Rueckgabewert beendet.
|
||||
|
||||
Der Timeoutwert bricht beim Erreichen das Warten sofort mit
|
||||
Wert 2 Rueckgabewert ab. (Das Timeout wird ueber die Zykluszeit
|
||||
der auto_refresh Funktion berechnet, entspricht also nicht exact den
|
||||
angegeben Millisekunden! Es wird immer nach oben gerundet!)
|
||||
|
||||
@param edge Flanke RISING, FALLING, BOTH bei der mit True beendet wird
|
||||
@param exitevent thrading.Event() fuer vorzeitiges Beenden mit False
|
||||
@param okvalue IO-Wert, bei dem das Warten sofort mit True beendet wird
|
||||
@param timeout Zeit in ms nach der mit False abgebrochen wird
|
||||
@return int() erfolgreich Werte <= 0
|
||||
- Erfolgreich gewartet
|
||||
Wert 0: IO hat den Wert gewechselt
|
||||
Wert -1: okvalue stimmte mit IO ueberein
|
||||
- Fehlerhaft gewartet
|
||||
Wert 1: exitevent wurde gesetzt
|
||||
Wert 2: timeout abgelaufen
|
||||
Wert 100: Devicelist.exit() wurde aufgerufen
|
||||
|
||||
"""
|
||||
# Prüfen ob Device in auto_refresh ist
|
||||
if not self._parentdevice._selfupdate:
|
||||
raise RuntimeError(
|
||||
"auto_refresh is not activated for device '{}|{}' - there "
|
||||
"will never be new data".format(
|
||||
self._parentdevice.position, self._parentdevice.name
|
||||
)
|
||||
)
|
||||
|
||||
if edge != BOTH and self._bitaddress < 0:
|
||||
raise AttributeError(
|
||||
"parameter 'edge' can be used with bit Inputs only"
|
||||
)
|
||||
|
||||
# Abbruchwert prüfen
|
||||
if okvalue == self.value:
|
||||
return -1
|
||||
|
||||
# WaitExit Event säubern
|
||||
self._parentdevice._parent._waitexit.clear()
|
||||
|
||||
val_start = self.value
|
||||
timeout = timeout / 1000
|
||||
bool_timecount = timeout > 0
|
||||
if exitevent is None:
|
||||
exitevent = Event()
|
||||
|
||||
flt_timecount = 0 if bool_timecount else -1
|
||||
while not self._parentdevice._parent._waitexit.is_set() \
|
||||
and not exitevent.is_set() \
|
||||
and flt_timecount < timeout:
|
||||
|
||||
if self._parentdevice._parent.imgwriter.newdata.wait(2.5):
|
||||
self._parentdevice._parent.imgwriter.newdata.clear()
|
||||
|
||||
if val_start != self.value:
|
||||
if edge == BOTH \
|
||||
or edge == RISING and not val_start \
|
||||
or edge == FALLING and val_start:
|
||||
return 0
|
||||
else:
|
||||
val_start = not val_start
|
||||
if bool_timecount:
|
||||
flt_timecount += \
|
||||
self._parentdevice._parent.imgwriter._refresh
|
||||
elif bool_timecount:
|
||||
# TODO: Prüfen
|
||||
flt_timecount += 1
|
||||
|
||||
# Abbruchevent wurde gesetzt
|
||||
if exitevent.is_set():
|
||||
return 1
|
||||
|
||||
# RevPiModIO mainloop wurde verlassen
|
||||
if self._parentdevice._parent._waitexit.is_set():
|
||||
return 100
|
||||
|
||||
# Timeout abgelaufen
|
||||
return 2
|
||||
|
||||
address = property(get_address)
|
||||
length = property(get_length)
|
||||
name = property(get_name)
|
||||
value = property(get_value, set_value)
|
||||
|
||||
|
||||
class IntIO(IOBase):
|
||||
|
||||
"""Klasse fuer den Zugriff auf die Daten mit Konvertierung in int().
|
||||
|
||||
Diese Klasse erweitert die Funktion von IOBase() um Funktionen,
|
||||
ueber die mit int() Werten gearbeitet werden kann. Fuer die Umwandlung
|
||||
koennen 'Byteorder' (Default 'little') und 'signed' (Default False) als
|
||||
Parameter gesetzt werden.
|
||||
@see #IOBase IOBase
|
||||
|
||||
"""
|
||||
|
||||
def __int__(self):
|
||||
"""Gibt IO als int() Wert zurueck mit Beachtung byteorder/signed.
|
||||
@return int() ohne Vorzeichen"""
|
||||
return self.get_int()
|
||||
|
||||
def _get_signed(self):
|
||||
"""Ruft ab, ob der Wert Vorzeichenbehaftet behandelt werden soll.
|
||||
@return True, wenn Vorzeichenbehaftet"""
|
||||
return self._signed
|
||||
|
||||
def _set_byteorder(self, value):
|
||||
"""Setzt Byteorder fuer int() Umwandlung.
|
||||
@param value str() 'little' or 'big'"""
|
||||
if not (value == "little" or value == "big"):
|
||||
raise ValueError("byteorder must be 'little' or 'big'")
|
||||
self._byteorder = value
|
||||
|
||||
def _set_signed(self, value):
|
||||
"""Left fest, ob der Wert Vorzeichenbehaftet behandelt werden soll.
|
||||
@param value True, wenn mit Vorzeichen behandel"""
|
||||
if type(value) != bool:
|
||||
raise ValueError("signed must be bool() True or False")
|
||||
self._signed = value
|
||||
|
||||
def get_int(self):
|
||||
"""Gibt IO als int() Wert zurueck mit Beachtung byteorder/signed.
|
||||
@return int() Wert"""
|
||||
return int.from_bytes(
|
||||
self._parentdevice._ba_devdata[self.slc_address],
|
||||
byteorder=self._byteorder,
|
||||
signed=self._signed
|
||||
)
|
||||
|
||||
def set_int(self, value):
|
||||
"""Setzt IO mit Beachtung byteorder/signed.
|
||||
@param value int()"""
|
||||
if type(value) == int:
|
||||
self.set_value(value.to_bytes(
|
||||
self._length,
|
||||
byteorder=self._byteorder,
|
||||
signed=self._signed
|
||||
))
|
||||
else:
|
||||
raise ValueError(
|
||||
"need an int() value, but {} was given".format(type(value))
|
||||
)
|
||||
|
||||
byteorder = property(IOBase._get_byteorder, _set_byteorder)
|
||||
signed = property(_get_signed, _set_signed)
|
||||
value = property(get_int, set_int)
|
||||
|
||||
|
||||
class StructIO(IOBase):
|
||||
|
||||
"""Klasse fuer den Zugriff auf Daten ueber ein definierten struct().
|
||||
|
||||
Diese Klasse ueberschreibt get_value() und set_value() der IOBase()
|
||||
Klasse. Sie stellt ueber struct die Werte in der gewuenschten Formatierung
|
||||
bereit. Der struct-Formatwert wird bei der Instantiierung festgelegt.
|
||||
@see #IOBase IOBase
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, parentdevice, valuelist, iotype, byteorder, frm):
|
||||
"""Erweitert IOBase um struct-Formatierung.
|
||||
@see #IOBase.__init__ IOBase.__init__(...)"""
|
||||
super().__init__(parentdevice, valuelist, iotype, byteorder)
|
||||
self.frm = frm
|
||||
|
||||
def get_structvalue(self):
|
||||
"""Gibt den Wert mit struct Formatierung zurueck.
|
||||
@return Wert vom Typ der struct-Formatierung"""
|
||||
if self._bitaddress >= 0:
|
||||
return self.get_value()
|
||||
else:
|
||||
return struct.unpack(self.frm, self.get_value())[0]
|
||||
|
||||
def set_structvalue(self, value):
|
||||
"""Setzt den Wert mit struct Formatierung.
|
||||
@param value Wert vom Typ der struct-Formatierung"""
|
||||
if self._bitaddress >= 0:
|
||||
self.set_value(value)
|
||||
else:
|
||||
self.set_value(struct.pack(self.frm, value))
|
||||
|
||||
byteorder = property(IOBase._get_byteorder)
|
||||
value = property(get_structvalue, set_structvalue)
|
||||
899
revpimodio2/modio.py
Normal file
899
revpimodio2/modio.py
Normal file
@@ -0,0 +1,899 @@
|
||||
#
|
||||
# python3-RevPiModIO
|
||||
#
|
||||
# Webpage: https://revpimodio.org/
|
||||
# (c) Sven Sager, License: LGPLv3
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
import warnings
|
||||
|
||||
from . import app as appmodule
|
||||
from . import device as devicemodule
|
||||
from . import helper as helpermodule
|
||||
from . import io as iomodule
|
||||
from . import summary as summarymodule
|
||||
|
||||
from .__init__ import RISING, FALLING, BOTH
|
||||
|
||||
from json import load as jload
|
||||
from os import access, F_OK, R_OK
|
||||
from signal import signal, SIG_DFL, SIGINT, SIGTERM
|
||||
from threading import Thread, Event
|
||||
|
||||
|
||||
class RevPiModIO(object):
|
||||
|
||||
"""Klasse fuer die Verwaltung aller piCtory Informationen.
|
||||
|
||||
Diese Klasse uebernimmt die gesamte Konfiguration aus piCtory und bilded
|
||||
die Devices und IOs ab. Sie uebernimmt die exklusive Verwaltung des
|
||||
Prozessabbilds und stellt sicher, dass die Daten synchron sind.
|
||||
Sollten nur einzelne Devices gesteuert werden, verwendet man
|
||||
RevPiModIOSelected() und uebergibt bei Instantiierung eine Liste mit
|
||||
Device Positionen oder Device Namen.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Instantiiert die Grundfunktionen.
|
||||
|
||||
@param kwargs Weitere Parameter:
|
||||
- auto_refresh: Wenn True, alle Devices zu auto_refresh hinzufuegen
|
||||
- configrsc: Pfad zur piCtory Konfigurationsdatei
|
||||
- procimg: Pfad zum Prozessabbild
|
||||
- monitoring: In- und Outputs werden gelesen, niemals geschrieben
|
||||
- simulator: Laed das Modul als Simulator und vertauscht IOs
|
||||
- syncoutputs: Aktuell gesetzte Outputs vom Prozessabbild einlesen
|
||||
|
||||
"""
|
||||
self._auto_refresh = kwargs.get("auto_refresh", False)
|
||||
self._configrsc = kwargs.get("configrsc", None)
|
||||
self._monitoring = kwargs.get("monitoring", False)
|
||||
self._procimg = kwargs.get("procimg", "/dev/piControl0")
|
||||
self._simulator = kwargs.get("simulator", False)
|
||||
self._syncoutputs = kwargs.get("syncoutputs", True)
|
||||
|
||||
# TODO: bei simulator und procimg prüfen ob datei existiert / anlegen?
|
||||
|
||||
# Private Variablen
|
||||
self.__cleanupfunc = None
|
||||
self._buffedwrite = False
|
||||
self._device = []
|
||||
self._exit = Event()
|
||||
self._imgwriter = None
|
||||
self._length = 0
|
||||
self._looprunning = False
|
||||
self._lst_devselect = []
|
||||
self._lst_refresh = []
|
||||
self._myfh = self._create_myfh()
|
||||
self._th_mainloop = None
|
||||
self._waitexit = Event()
|
||||
|
||||
# Modulvariablen
|
||||
self.core = None
|
||||
|
||||
# piCtory Klassen
|
||||
self.app = None
|
||||
self.device = None
|
||||
self.devices = None
|
||||
self.io = None
|
||||
self.summary = None
|
||||
|
||||
# Nur Konfigurieren, wenn nicht vererbt
|
||||
if type(self) == RevPiModIO:
|
||||
self._configure()
|
||||
|
||||
def __del__(self):
|
||||
"""Zerstoert alle Klassen um aufzuraeumen."""
|
||||
self.exit(full=True)
|
||||
self._myfh.close()
|
||||
|
||||
def __evt_exit(self, signum, sigframe):
|
||||
"""Eventhandler fuer Programmende.
|
||||
@param signum Signalnummer
|
||||
@param sigframe Signalframe"""
|
||||
signal(SIGINT, SIG_DFL)
|
||||
signal(SIGTERM, SIG_DFL)
|
||||
self.exit(full=True)
|
||||
if self.__cleanupfunc is not None:
|
||||
self.readprocimg()
|
||||
self.__cleanupfunc()
|
||||
self.writeprocimg()
|
||||
|
||||
def _configure(self):
|
||||
"""Verarbeitet die piCtory Konfigurationsdatei."""
|
||||
jconfigrsc = self.get_jconfigrsc()
|
||||
|
||||
# App Klasse instantiieren
|
||||
self.app = appmodule.App(jconfigrsc["App"])
|
||||
|
||||
# Devicefilter anwenden
|
||||
if len(self._lst_devselect) > 0:
|
||||
lst_found = []
|
||||
|
||||
if type(self) == RevPiModIODriver:
|
||||
_searchtype = "VIRTUAL"
|
||||
else:
|
||||
_searchtype = None
|
||||
|
||||
# Angegebene Devices suchen
|
||||
for dev in jconfigrsc["Devices"]:
|
||||
if _searchtype is None or dev["type"] == _searchtype:
|
||||
if dev["name"] in self._lst_devselect:
|
||||
lst_found.append(dev)
|
||||
elif dev["position"].isnumeric() \
|
||||
and int(dev["position"]) in self._lst_devselect:
|
||||
lst_found.append(dev)
|
||||
|
||||
# Devices aus JSON oder Filter übernehmen
|
||||
lst_devices = jconfigrsc["Devices"] if len(self._lst_devselect) == 0 \
|
||||
else lst_found
|
||||
|
||||
# Device und IO Klassen anlegen
|
||||
self.device = devicemodule.DeviceList()
|
||||
self.io = iomodule.IOList()
|
||||
|
||||
# Devices initialisieren
|
||||
err_names = []
|
||||
for device in sorted(lst_devices, key=lambda x: x["position"]):
|
||||
|
||||
# Bei VDev in alter piCtory Version, Position eindeutig machen
|
||||
if device["position"] == "adap.":
|
||||
device["position"] = -1
|
||||
# NOTE: Testen mit alter piCtory Version
|
||||
while device["position"] in self.device:
|
||||
device["position"] -= 1
|
||||
|
||||
if device["type"] == "BASE":
|
||||
# Core
|
||||
dev_new = devicemodule.Core(
|
||||
self, device, simulator=self._simulator
|
||||
)
|
||||
self.core = dev_new
|
||||
|
||||
# Für RS485 errors defaults laden und schreiben
|
||||
# NOTE: Soll das wirklich gemacht werden?
|
||||
for io in dev_new.get_outs():
|
||||
io.set_value(io.defaultvalue)
|
||||
if not self._monitoring:
|
||||
self.writeprocimg(True, dev_new)
|
||||
|
||||
elif device["type"] == "LEFT_RIGHT":
|
||||
# IOs
|
||||
dev_new = devicemodule.Device(
|
||||
self, device, simulator=self._simulator
|
||||
)
|
||||
elif device["type"] == "VIRTUAL":
|
||||
# Virtuals
|
||||
dev_new = devicemodule.Virtual(
|
||||
self, device, simulator=self._simulator
|
||||
)
|
||||
elif device["type"] == "EDGE":
|
||||
# Gateways
|
||||
dev_new = devicemodule.Gateway(
|
||||
self, device, simulator=self._simulator
|
||||
)
|
||||
else:
|
||||
# Device-Type nicht gefunden
|
||||
warnings.warn(
|
||||
"device type {} unknown",
|
||||
Warning
|
||||
)
|
||||
dev_new = None
|
||||
|
||||
if dev_new is not None:
|
||||
self._device.append(dev_new)
|
||||
|
||||
# Offset prüfen, muss mit Länge übereinstimmen
|
||||
if self._length < dev_new.offset:
|
||||
self._length = dev_new.offset
|
||||
|
||||
self._length += dev_new._length
|
||||
|
||||
# Auf doppelte Namen prüfen, da piCtory dies zulässt
|
||||
if hasattr(self.device, dev_new.name):
|
||||
err_names.append(dev_new.name)
|
||||
|
||||
# DeviceList für direkten Zugriff aufbauen
|
||||
setattr(self.device, dev_new.name, dev_new)
|
||||
|
||||
# dict_devname zerstören, wenn doppelte Namen vorhanden sind
|
||||
for errdev in err_names:
|
||||
delattr(self.device, errdev)
|
||||
warnings.warn(
|
||||
"equal device name in pictory configuration. can not "
|
||||
"build device to acces by name. you can access all devices "
|
||||
"by position number pos_XX only!",
|
||||
Warning
|
||||
)
|
||||
|
||||
# ImgWriter erstellen
|
||||
self._imgwriter = helpermodule.ProcimgWriter(self)
|
||||
|
||||
# Aktuellen Outputstatus von procimg einlesen
|
||||
if self._syncoutputs:
|
||||
self.syncoutputs(force=True)
|
||||
|
||||
# NOTE: Nur noch bis Final für kompatibilität
|
||||
# Devices Klasse instantiieren
|
||||
self.devices = devicemodule.Devicelist(self)
|
||||
|
||||
# Optional ins auto_refresh aufnehmen
|
||||
if self._auto_refresh:
|
||||
for dev in self._device:
|
||||
dev.auto_refresh()
|
||||
|
||||
# Summary Klasse instantiieren
|
||||
self.summary = summarymodule.Summary(jconfigrsc["Summary"])
|
||||
|
||||
def _create_myfh(self):
|
||||
"""Erstellt FileObject mit Pfad zum procimg.
|
||||
return FileObject"""
|
||||
self._buffedwrite = False
|
||||
return open(self._procimg, "r+b", 0)
|
||||
|
||||
def _get_configrsc(self):
|
||||
"""Getter function.
|
||||
@return Pfad der verwendeten piCtory Konfiguration"""
|
||||
return self._configrsc
|
||||
|
||||
def _get_cycletime(self):
|
||||
"""Gibt Aktualisierungsrate in ms der Prozessabbildsynchronisierung aus.
|
||||
@return Millisekunden"""
|
||||
return self._imgwriter.refresh
|
||||
|
||||
def _get_length(self):
|
||||
"""Getter function.
|
||||
@return Laenge in Bytes der Devices"""
|
||||
return self._length
|
||||
|
||||
def _get_monitoring(self):
|
||||
"""Getter function.
|
||||
@return True, wenn als Monitoring gestartet"""
|
||||
return self._monitoring
|
||||
|
||||
def _get_procimg(self):
|
||||
"""Getter function.
|
||||
@return Pfad des verwendeten Prozessabbilds"""
|
||||
return self._procimg
|
||||
|
||||
def _get_simulator(self):
|
||||
"""Getter function.
|
||||
@return True, wenn als Simulator gestartet"""
|
||||
return self._simulator
|
||||
|
||||
def _set_cycletime(self, milliseconds):
|
||||
"""Setzt Aktualisierungsrate der Prozessabbild-Synchronisierung.
|
||||
@param milliseconds int() in Millisekunden"""
|
||||
self._imgwriter.refresh = milliseconds
|
||||
|
||||
def auto_refresh_maxioerrors(self, value=None):
|
||||
"""Maximale IO Fehler fuer auto_refresh.
|
||||
@param value Setzt maximale Anzahl bis exception ausgeloest wird
|
||||
@return Maximale Anzahl bis exception ausgeloest wird"""
|
||||
if value is None:
|
||||
return self._imgwriter.maxioerrors
|
||||
elif type(value) == int and value >= 0:
|
||||
self._imgwriter.maxioerrors = value
|
||||
|
||||
def auto_refresh_resetioerrors(self):
|
||||
"""Setzt aktuellen IOError-Zaehler auf 0 zurueck."""
|
||||
self._imgwriter.maxioerrors = 0
|
||||
|
||||
def cleanup(self):
|
||||
"""Beendet auto_refresh und alle Threads."""
|
||||
# TODO: wirklich alles löschen
|
||||
self.exit(full=True)
|
||||
self._myfh.close()
|
||||
self.app = None
|
||||
self.device = None
|
||||
self.devices = None
|
||||
self.io = None
|
||||
self.summary = None
|
||||
|
||||
def cycleloop(self, func, cycletime=50):
|
||||
"""Startet den Cycleloop.
|
||||
|
||||
Der aktuelle Programmthread wird hier bis Aufruf von
|
||||
RevPiDevicelist.exit() "gefangen". Er fuehrt nach jeder Aktualisierung
|
||||
des Prozessabbilds die uebergebene Funktion "func" aus und arbeitet sie
|
||||
ab. Waehrend der Ausfuehrung der Funktion wird das Prozessabbild nicht
|
||||
weiter aktualisiert. Die Inputs behalten bis zum Ende den aktuellen
|
||||
Wert. Gesetzte Outputs werden nach Ende des Funktionsdurchlaufs in das
|
||||
Prozessabbild geschrieben.
|
||||
|
||||
Verlassen wird der Cycleloop, wenn die aufgerufene Funktion einen
|
||||
Rueckgabewert nicht gleich None liefert, oder durch Aufruf von
|
||||
revpimodio.exit().
|
||||
|
||||
HINWEIS: Die Aktualisierungszeit und die Laufzeit der Funktion duerfen
|
||||
die eingestellte auto_refresh Zeit, bzw. uebergebene cycletime nicht
|
||||
ueberschreiten!
|
||||
|
||||
Ueber den Parameter cycletime kann die Aktualisierungsrate fuer das
|
||||
Prozessabbild gesetzt werden (selbe Funktion wie
|
||||
set_refreshtime(milliseconds)).
|
||||
|
||||
@param func Funktion, die ausgefuehrt werden soll
|
||||
@param cycletime auto_refresh Wert in Millisekunden
|
||||
@return None
|
||||
|
||||
"""
|
||||
# Prüfen ob ein Loop bereits läuft
|
||||
if self._looprunning:
|
||||
raise RuntimeError(
|
||||
"can not start multiple loops mainloop/cycleloop"
|
||||
)
|
||||
|
||||
# Prüfen ob Devices in auto_refresh sind
|
||||
if len(self._lst_refresh) == 0:
|
||||
raise RuntimeError("no device with auto_refresh activated")
|
||||
|
||||
# Prüfen ob Funktion callable ist
|
||||
if not callable(func):
|
||||
raise RuntimeError(
|
||||
"registered function '{}' ist not callable".format(func)
|
||||
)
|
||||
|
||||
# Zykluszeit übernehmen
|
||||
if cycletime != self._imgwriter.refresh:
|
||||
self._imgwriter.refresh = cycletime
|
||||
|
||||
# Cycleloop starten
|
||||
self._looprunning = True
|
||||
cycleinfo = helpermodule.Cycletools()
|
||||
ec = None
|
||||
while ec is None and not self._exit.is_set():
|
||||
# Auf neue Daten warten und nur ausführen wenn set()
|
||||
if not self._imgwriter.newdata.wait(2.5):
|
||||
if not self._exit.is_set() and not self._imgwriter.is_alive():
|
||||
raise RuntimeError("auto_refresh thread not running")
|
||||
continue
|
||||
self._imgwriter.newdata.clear()
|
||||
|
||||
# Vor Aufruf der Funktion auto_refresh sperren
|
||||
self._imgwriter.lck_refresh.acquire()
|
||||
|
||||
# Funktion aufrufen und auswerten
|
||||
ec = func(cycleinfo)
|
||||
cycleinfo._docycle()
|
||||
|
||||
# auto_refresh freigeben
|
||||
self._imgwriter.lck_refresh.release()
|
||||
|
||||
# Cycleloop beenden
|
||||
self._looprunning = False
|
||||
|
||||
return ec
|
||||
|
||||
def exit(self, full=True):
|
||||
"""Beendet mainloop() und optional auto_refresh.
|
||||
|
||||
Wenn sich das Programm im mainloop() befindet, wird durch Aufruf
|
||||
von exit() die Kontrolle wieder an das Hauptprogramm zurueckgegeben.
|
||||
|
||||
Der Parameter full ist mit True vorbelegt und entfernt alle Devices aus
|
||||
dem auto_refresh. Der Thread fuer die Prozessabbildsynchronisierung
|
||||
wird dann gestoppt und das Programm kann sauber beendet werden.
|
||||
|
||||
@param full Entfernt auch alle Devices aus auto_refresh"""
|
||||
self._exit.set()
|
||||
self._waitexit.set()
|
||||
if full:
|
||||
if self._imgwriter.is_alive():
|
||||
self._imgwriter.stop()
|
||||
self._imgwriter.join(self._imgwriter._refresh)
|
||||
while len(self._lst_refresh) > 0:
|
||||
dev = self._lst_refresh.pop()
|
||||
dev._selfupdate = False
|
||||
if not self._monitoring:
|
||||
self.writeprocimg(True, dev)
|
||||
|
||||
def get_jconfigrsc(self):
|
||||
"""Laed die piCotry Konfiguration und erstellt ein dict().
|
||||
@return dict() der piCtory Konfiguration"""
|
||||
# piCtory Konfiguration prüfen
|
||||
if self._configrsc is not None:
|
||||
if not access(self._configrsc, F_OK | R_OK):
|
||||
raise RuntimeError(
|
||||
"can not access pictory configuration at {}".format(
|
||||
self._configrsc))
|
||||
else:
|
||||
# piCtory Konfiguration an bekannten Stellen prüfen
|
||||
lst_rsc = ["/etc/revpi/config.rsc", "/opt/KUNBUS/config.rsc"]
|
||||
for rscfile in lst_rsc:
|
||||
if access(rscfile, F_OK | R_OK):
|
||||
self._configrsc = rscfile
|
||||
break
|
||||
if self._configrsc is None:
|
||||
raise RuntimeError(
|
||||
"can not access known pictory configurations at {} - "
|
||||
"use 'configrsc' parameter so specify location"
|
||||
"".format(", ".join(lst_rsc))
|
||||
)
|
||||
|
||||
with open(self._configrsc, "r") as fhconfigrsc:
|
||||
return jload(fhconfigrsc)
|
||||
|
||||
def handlesignalend(self, cleanupfunc=None):
|
||||
"""Signalhandler fuer Programmende verwalten.
|
||||
|
||||
Wird diese Funktion aufgerufen, uebernimmt RevPiModIO die SignalHandler
|
||||
fuer SIGINT und SIGTERM. Diese werden Empfangen, wenn das
|
||||
Betriebssystem oder der Benutzer das Steuerungsprogramm sauber beenden
|
||||
will.
|
||||
|
||||
Die optionale Funktion "cleanupfunc" wird als letztes nach dem letzten
|
||||
Einlesen der Inputs ausgefuehrt. Dort gesetzte Outputs werden nach
|
||||
Ablauf der Funktion ein letztes Mal geschrieben.
|
||||
Gedacht ist dies fuer Aufraeumarbeiten, wie z.B. das abschalten der
|
||||
LEDs am RevPi-Core.
|
||||
|
||||
Nach einmaligem Empfangen eines der Signale und dem Beenden der
|
||||
RevPiModIO Thrads / Funktionen werden die SignalHandler wieder
|
||||
freigegeben.
|
||||
|
||||
@param cleanupfunc Funktion wird nach dem letzten Lesen der Inputs
|
||||
ausgefuehrt, gefolgt vom letzten Schreiben der Outputs
|
||||
|
||||
"""
|
||||
# Prüfen ob Funktion callable ist
|
||||
if not callable(cleanupfunc):
|
||||
raise RuntimeError(
|
||||
"registered function '{}' ist not callable".format(cleanupfunc)
|
||||
)
|
||||
self.__cleanupfunc = cleanupfunc
|
||||
signal(SIGINT, self.__evt_exit)
|
||||
signal(SIGTERM, self.__evt_exit)
|
||||
|
||||
def mainloop(self, freeze=False, blocking=True):
|
||||
"""Startet den Mainloop mit Eventueberwachung.
|
||||
|
||||
Der aktuelle Programmthread wird hier bis Aufruf von
|
||||
RevPiDevicelist.exit() "gefangen" (es sei denn blocking=False). Er
|
||||
durchlaeuft die Eventueberwachung und prueft Aenderungen der, mit
|
||||
einem Event registrierten, IOs. Wird eine Veraenderung erkannt,
|
||||
fuert das Programm die dazugehoerigen Funktionen der Reihe nach aus.
|
||||
|
||||
Wenn der Parameter "freeze" mit True angegeben ist, wird die
|
||||
Prozessabbildsynchronisierung angehalten bis alle Eventfunktionen
|
||||
ausgefuehrt wurden. Inputs behalten fuer die gesamte Dauer ihren
|
||||
aktuellen Wert und Outputs werden erst nach Durchlauf aller Funktionen
|
||||
in das Prozessabbild geschrieben.
|
||||
|
||||
Wenn der Parameter "blocking" mit False angegeben wird, aktiviert
|
||||
dies die Eventueberwachung und blockiert das Programm NICHT an der
|
||||
Stelle des Aufrufs. Eignet sich gut fuer die GUI Programmierung, wenn
|
||||
Events vom RevPi benoetigt werden, aber das Programm weiter ausgefuehrt
|
||||
werden soll.
|
||||
|
||||
@param freeze Wenn True, Prozessabbildsynchronisierung anhalten
|
||||
@param blocking Wenn False, blockiert das Programm NICHT
|
||||
@return None
|
||||
|
||||
"""
|
||||
# Prüfen ob ein Loop bereits läuft
|
||||
if self._looprunning:
|
||||
raise RuntimeError(
|
||||
"can not start multiple loops mainloop/cycleloop"
|
||||
)
|
||||
|
||||
# Prüfen ob Devices in auto_refresh sind
|
||||
if len(self._lst_refresh) == 0:
|
||||
raise RuntimeError("no device with auto_refresh activated")
|
||||
|
||||
# Thread erstellen, wenn nicht blockieren soll
|
||||
if not blocking:
|
||||
self._th_mainloop = Thread(
|
||||
target=self.mainloop,
|
||||
kwargs={"freeze": freeze, "blocking": True}
|
||||
)
|
||||
self._th_mainloop.start()
|
||||
return
|
||||
|
||||
# Event säubern vor Eintritt in Mainloop
|
||||
self._exit.clear()
|
||||
self._looprunning = True
|
||||
|
||||
# Beim Eintritt in mainloop Bytecopy erstellen
|
||||
for dev in self._lst_refresh:
|
||||
dev._filelock.acquire()
|
||||
dev._ba_datacp = dev._ba_devdata[:]
|
||||
dev._filelock.release()
|
||||
|
||||
lst_fire = []
|
||||
while not self._exit.is_set():
|
||||
|
||||
# Auf neue Daten warten und nur ausführen wenn set()
|
||||
if not self._imgwriter.newdata.wait(2.5):
|
||||
if not self._exit.is_set() and not self._imgwriter.is_alive():
|
||||
raise RuntimeError("auto_refresh thread not running")
|
||||
continue
|
||||
|
||||
self._imgwriter.newdata.clear()
|
||||
|
||||
# Während Auswertung refresh sperren
|
||||
self._imgwriter.lck_refresh.acquire()
|
||||
|
||||
for dev in self._lst_refresh:
|
||||
|
||||
if len(dev._dict_events) == 0 \
|
||||
or dev._ba_datacp == dev._ba_devdata:
|
||||
continue
|
||||
|
||||
for io_event in dev._dict_events:
|
||||
|
||||
if dev._ba_datacp[io_event.slc_address] == \
|
||||
dev._ba_devdata[io_event.slc_address]:
|
||||
continue
|
||||
|
||||
if io_event._bitaddress >= 0:
|
||||
boolcp = bool(int.from_bytes(
|
||||
dev._ba_datacp[io_event.slc_address],
|
||||
byteorder=io_event._byteorder
|
||||
) & 1 << io_event._bitaddress)
|
||||
boolor = bool(int.from_bytes(
|
||||
dev._ba_devdata[io_event.slc_address],
|
||||
byteorder=io_event._byteorder
|
||||
) & 1 << io_event._bitaddress)
|
||||
|
||||
if boolor == boolcp:
|
||||
continue
|
||||
|
||||
for regfunc in dev._dict_events[io_event]:
|
||||
if regfunc[1] == BOTH \
|
||||
or regfunc[1] == RISING and boolor \
|
||||
or regfunc[1] == FALLING and not boolor:
|
||||
lst_fire.append(
|
||||
(regfunc, io_event.name, io_event.value)
|
||||
)
|
||||
|
||||
else:
|
||||
for regfunc in dev._dict_events[io_event]:
|
||||
lst_fire.append(
|
||||
(regfunc, io_event.name, io_event.value)
|
||||
)
|
||||
|
||||
# Nach Verarbeitung aller IOs die Bytes kopieren
|
||||
dev._filelock.acquire()
|
||||
dev._ba_datacp = dev._ba_devdata[:]
|
||||
dev._filelock.release()
|
||||
|
||||
# Refreshsperre aufheben wenn nicht freeze
|
||||
if not freeze:
|
||||
self._imgwriter.lck_refresh.release()
|
||||
|
||||
# Erst nach Datenübernahme alle Events feuern
|
||||
while len(lst_fire) > 0:
|
||||
tup_fire = lst_fire.pop()
|
||||
event_func = tup_fire[0][0]
|
||||
passname = tup_fire[1]
|
||||
passvalue = tup_fire[2]
|
||||
if tup_fire[0][2]:
|
||||
th = helpermodule.EventCallback(
|
||||
event_func, passname, passvalue
|
||||
)
|
||||
th.start()
|
||||
else:
|
||||
# Direct callen da Prüfung in RevPiDevice.reg_event ist
|
||||
event_func(passname, passvalue)
|
||||
|
||||
# Refreshsperre aufheben wenn freeze
|
||||
if freeze:
|
||||
self._imgwriter.lck_refresh.release()
|
||||
|
||||
# Mainloop verlassen
|
||||
self._looprunning = False
|
||||
|
||||
def readprocimg(self, force=False, device=None):
|
||||
"""Einlesen aller Inputs aller/eines Devices vom Prozessabbild.
|
||||
|
||||
@param force auch Devices mit autoupdate=False
|
||||
@param device nur auf einzelnes Device anwenden
|
||||
@return True, wenn Arbeiten an allen Devices erfolgreich waren
|
||||
|
||||
"""
|
||||
if device is None:
|
||||
mylist = self._device
|
||||
else:
|
||||
# TODO: Devicesuchen ändern
|
||||
dev = device if issubclass(type(device), devicemodule.Device) \
|
||||
else self.device.__getitem__(device)
|
||||
|
||||
if dev._selfupdate:
|
||||
raise RuntimeError(
|
||||
"can not read process image, while device '{}|{}'"
|
||||
"is in auto_refresh mode".format(dev.position, dev.name)
|
||||
)
|
||||
mylist = [dev]
|
||||
|
||||
# Daten komplett einlesen
|
||||
try:
|
||||
self._myfh.seek(0)
|
||||
bytesbuff = self._myfh.read(self._length)
|
||||
except IOError:
|
||||
warnings.warn(
|
||||
"read error on process image '{}'".format(self.myfh.name),
|
||||
RuntimeWarning
|
||||
)
|
||||
return False
|
||||
|
||||
for dev in mylist:
|
||||
if (force or dev.autoupdate) and not dev._selfupdate:
|
||||
|
||||
# FileHandler sperren
|
||||
dev._filelock.acquire()
|
||||
|
||||
if self._monitoring:
|
||||
# Alles vom Bus einlesen
|
||||
dev._ba_devdata[:] = bytesbuff[dev.slc_devoff]
|
||||
else:
|
||||
# Inputs vom Bus einlesen
|
||||
dev._ba_devdata[dev.slc_inp] = bytesbuff[dev.slc_inpoff]
|
||||
|
||||
# Mems vom Bus lesen
|
||||
dev._ba_devdata[dev.slc_mem] = bytesbuff[dev.slc_memoff]
|
||||
|
||||
dev._filelock.release()
|
||||
|
||||
return True
|
||||
|
||||
def setdefaultvalues(self, force=False, device=None):
|
||||
"""Alle Outputbuffer werden auf die piCtory default Werte gesetzt.
|
||||
@param force auch Devices mit autoupdate=False
|
||||
@param device nur auf einzelnes Device anwenden"""
|
||||
if self._monitoring:
|
||||
raise RuntimeError(
|
||||
"can not set default values, while system is in monitoring "
|
||||
"mode"
|
||||
)
|
||||
|
||||
if device is None:
|
||||
mylist = self._device
|
||||
else:
|
||||
dev = device if issubclass(type(device), devicemodule.Device) \
|
||||
else self.__getitem__(device)
|
||||
mylist = [dev]
|
||||
|
||||
for dev in mylist:
|
||||
if (force or dev.autoupdate):
|
||||
for io in dev.get_outs():
|
||||
io.set_value(io.defaultvalue)
|
||||
|
||||
def syncoutputs(self, force=False, device=None):
|
||||
"""Lesen aller aktuell gesetzten Outputs im Prozessabbild.
|
||||
|
||||
@param force auch Devices mit autoupdate=False
|
||||
@param device nur auf einzelnes Device anwenden
|
||||
@return True, wenn Arbeiten an allen Devices erfolgreich waren
|
||||
|
||||
"""
|
||||
if device is None:
|
||||
mylist = self._device
|
||||
else:
|
||||
dev = device if issubclass(type(device), devicemodule.Device) \
|
||||
else self.__getitem__(device)
|
||||
|
||||
if dev._selfupdate:
|
||||
raise RuntimeError(
|
||||
"can not sync process image, while device '{}|{}'"
|
||||
"is in auto_refresh mode".format(dev.position, dev.name)
|
||||
)
|
||||
mylist = [dev]
|
||||
|
||||
try:
|
||||
self._myfh.seek(0)
|
||||
bytesbuff = self._myfh.read(self._length)
|
||||
except IOError:
|
||||
warnings.warn(
|
||||
"read error on process image '{}'".format(self._myfh.name),
|
||||
RuntimeWarning
|
||||
)
|
||||
return False
|
||||
|
||||
for dev in mylist:
|
||||
if (force or dev.autoupdate) and not dev._selfupdate:
|
||||
dev._filelock.acquire()
|
||||
# Outputs vom Bus einlesen
|
||||
dev._ba_devdata[dev.slc_out] = bytesbuff[dev.slc_outoff]
|
||||
dev._filelock.release()
|
||||
return True
|
||||
|
||||
def writedefaultinputs(self, virtual_device):
|
||||
"""Schreibt fuer ein virtuelles Device piCtory Defaultinputwerte.
|
||||
|
||||
Sollten in piCtory Defaultwerte fuer Inputs eines virtuellen Devices
|
||||
angegeben sein, werden diese nur beim Systemstart oder einem piControl
|
||||
Reset gesetzt. Sollte danach das Prozessabbild mit NULL ueberschrieben,
|
||||
gehen diese Werte verloren.
|
||||
Diese Funktion kann nur auf virtuelle Devices angewendet werden!
|
||||
|
||||
@param virtual_device Virtuelles Device fuer Wiederherstellung
|
||||
@return True, wenn Arbeiten am virtuellen Device erfolgreich waren
|
||||
|
||||
"""
|
||||
if self._monitoring:
|
||||
raise RuntimeError(
|
||||
"can not write process image, while system is in monitoring "
|
||||
"mode"
|
||||
)
|
||||
|
||||
# Device suchen
|
||||
dev = virtual_device if issubclass(type(virtual_device), devicemodule.Device) \
|
||||
else self.__getitem__(virtual_device)
|
||||
|
||||
# Prüfen ob es ein virtuelles Device ist
|
||||
if not issubclass(type(dev), devicemodule.Virtual):
|
||||
raise RuntimeError(
|
||||
"this function can be used for virtual devices only"
|
||||
)
|
||||
|
||||
workokay = True
|
||||
dev._filelock.acquire()
|
||||
|
||||
for io in dev.get_inps():
|
||||
dev._ba_devdata[io.slc_address] = io.defaultvalue
|
||||
|
||||
# Outpus auf Bus schreiben
|
||||
try:
|
||||
self._myfh.seek(dev.slc_inpoff.start)
|
||||
self._myfh.write(dev._ba_devdata[dev.slc_inp])
|
||||
if self._buffedwrite:
|
||||
self._myfh.flush()
|
||||
except IOError:
|
||||
warnings.warn(
|
||||
"write error on process image '{}'"
|
||||
"".format(self._myfh.name),
|
||||
RuntimeWarning
|
||||
)
|
||||
workokay = False
|
||||
|
||||
dev._filelock.release()
|
||||
return workokay
|
||||
|
||||
def writeprocimg(self, force=False, device=None):
|
||||
"""Schreiben aller Outputs aller Devices ins Prozessabbild.
|
||||
|
||||
@param force auch Devices mit autoupdate=False
|
||||
@param device nur auf einzelnes Device anwenden
|
||||
@return True, wenn Arbeiten an allen Devices erfolgreich waren
|
||||
|
||||
"""
|
||||
if self._monitoring:
|
||||
raise RuntimeError(
|
||||
"can not write process image, while system is in monitoring "
|
||||
"mode"
|
||||
)
|
||||
|
||||
if device is None:
|
||||
mylist = self._device
|
||||
else:
|
||||
dev = device if issubclass(type(device), devicemodule.Device) \
|
||||
else self.__getitem__(device)
|
||||
|
||||
if dev._selfupdate:
|
||||
raise RuntimeError(
|
||||
"can not write process image, while device '{}|{}'"
|
||||
"is in auto_refresh mode".format(dev.position, dev.name)
|
||||
)
|
||||
mylist = [dev]
|
||||
|
||||
workokay = True
|
||||
for dev in mylist:
|
||||
if (force or dev.autoupdate) and not dev._selfupdate:
|
||||
dev._filelock.acquire()
|
||||
|
||||
# Outpus auf Bus schreiben
|
||||
try:
|
||||
self._myfh.seek(dev.slc_outoff.start)
|
||||
self._myfh.write(dev._ba_devdata[dev.slc_out])
|
||||
except IOError:
|
||||
workokay = False
|
||||
|
||||
dev._filelock.release()
|
||||
|
||||
if self._buffedwrite:
|
||||
try:
|
||||
self._myfh.flush()
|
||||
except IOError:
|
||||
workokay = False
|
||||
|
||||
if not workokay:
|
||||
warnings.warn(
|
||||
"write error on process image '{}'"
|
||||
"".format(self._myfh.name),
|
||||
RuntimeWarning
|
||||
)
|
||||
|
||||
return workokay
|
||||
|
||||
configrsc = property(_get_configrsc)
|
||||
cycletime = property(_get_cycletime, _set_cycletime)
|
||||
length = property(_get_length)
|
||||
monitoring = property(_get_monitoring)
|
||||
procimg = property(_get_procimg)
|
||||
simulator = property(_get_simulator)
|
||||
|
||||
|
||||
class RevPiModIOSelected(RevPiModIO):
|
||||
|
||||
"""Klasse fuer die Verwaltung einzelner Devices aus piCtory.
|
||||
|
||||
Diese Klasse uebernimmt nur angegebene Devices der piCtory Konfiguration
|
||||
und bilded sie inkl. IOs ab. Sie uebernimmt die exklusive Verwaltung des
|
||||
Adressbereichs im Prozessabbild an dem sich die angegebenen Devices
|
||||
befinden und stellt sicher, dass die Daten synchron sind.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, deviceselection, **kwargs):
|
||||
"""Instantiiert nur fuer angegebene Devices die Grundfunktionen.
|
||||
|
||||
Der Parameter deviceselection kann eine einzelne
|
||||
Device Position / einzelner Device Name sein oder eine Liste mit
|
||||
mehreren Positionen / Namen
|
||||
|
||||
@param deviceselection Positionsnummer oder Devicename
|
||||
@param kwargs Weitere Parameter
|
||||
@see #RevPiModIO.__init__ RevPiModIO.__init__(...)
|
||||
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Device liste erstellen
|
||||
if type(deviceselection) == list:
|
||||
for dev in deviceselection:
|
||||
self._lst_devselect.append(dev)
|
||||
else:
|
||||
self._lst_devselect.append(deviceselection)
|
||||
|
||||
for vdev in self._lst_devselect:
|
||||
if type(vdev) != int and type(vdev) != str:
|
||||
raise ValueError(
|
||||
"need device position as int() or device name as str()"
|
||||
)
|
||||
|
||||
self._configure()
|
||||
|
||||
if len(self._device) == 0:
|
||||
if type(self) == RevPiModIODriver:
|
||||
raise RuntimeError(
|
||||
"could not find any given VIRTUAL devices in config"
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"could not find any given devices in config"
|
||||
)
|
||||
elif len(self._device) != len(self._lst_devselect):
|
||||
if type(self) == RevPiModIODriver:
|
||||
raise RuntimeError(
|
||||
"could not find all given VIRTUAL devices in config"
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"could not find all given devices in config"
|
||||
)
|
||||
|
||||
|
||||
class RevPiModIODriver(RevPiModIOSelected):
|
||||
|
||||
"""Klasse um eigene Treiber fuer die virtuellen Devices zu erstellen.
|
||||
|
||||
Mit dieser Klasse werden nur angegebene Virtuelle Devices mit RevPiModIO
|
||||
verwaltet. Bei Instantiierung werden automatisch die Inputs und Outputs
|
||||
verdreht, um das Schreiben der Inputs zu ermoeglichen. Die Daten koennen
|
||||
dann ueber logiCAD an den Devices abgerufen werden.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, vdev, **kwargs):
|
||||
"""Instantiiert die Grundfunktionen.
|
||||
|
||||
@param vdev Virtuelles Device fuer die Verwendung / oder list()
|
||||
@param kwargs Weitere Parameter (nicht monitoring und simulator)
|
||||
@see #RevPiModIO.__init__ RevPiModIO.__init__(...)
|
||||
|
||||
"""
|
||||
kwargs["monitoring"] = False
|
||||
kwargs["simulator"] = True
|
||||
super().__init__(vdev, **kwargs)
|
||||
19
revpimodio2/summary.py
Normal file
19
revpimodio2/summary.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# python3-RevPiModIO
|
||||
#
|
||||
# Webpage: https://revpimodio.org/
|
||||
# (c) Sven Sager, License: LGPLv3
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Bildet die Summary-Sektion von piCtory ab."""
|
||||
|
||||
|
||||
class Summary(object):
|
||||
|
||||
"""Bildet die Summary-Sektion der config.rsc ab."""
|
||||
|
||||
def __init__(self, summary):
|
||||
"""Instantiiert die RevPiSummary-Klasse.
|
||||
@param summary piCtory Summaryinformationen"""
|
||||
self.inptotal = summary["inpTotal"]
|
||||
self.outtotal = summary["outTotal"]
|
||||
41
setup.py
Normal file
41
setup.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# (c) Sven Sager, License: LGPLv3
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Setupscript fuer python3-revpimodio."""
|
||||
from distutils.core import setup
|
||||
|
||||
setup(
|
||||
author="Sven Sager",
|
||||
author_email="akira@narux.de",
|
||||
url="https://revpimodio.org",
|
||||
maintainer="Sven Sager",
|
||||
maintainer_email="akira@revpimodio.org",
|
||||
|
||||
license="LGPLv3",
|
||||
name="revpimodio2",
|
||||
version="2.0.0",
|
||||
|
||||
py_modules=["revpimodio2"],
|
||||
|
||||
description="Python3 Programmierung für Kunbus RevolutionPi",
|
||||
long_description=""
|
||||
"Das Modul stellt alle Devices und IOs aus der piCtory Konfiguration \n"
|
||||
"in Python3 zur Verfügung. Es ermöglicht den direkten Zugriff auf die \n"
|
||||
"Werte über deren vergebenen Namen. Lese- und Schreibaktionen mit dem \n"
|
||||
"Prozessabbild werden von dem Modul selbst verwaltet, ohne dass sich \n"
|
||||
"der Programmierer um Offsets und Adressen kümmern muss. Für die \n"
|
||||
"Gatewaymodule wie ModbusTCP oder Profinet sind eigene 'Inputs' und \n"
|
||||
"'Outputs' über einen bestimmten Adressbereich definierbar. Auf \n"
|
||||
"diese IOs kann mit Python3 über den Namen direkt auf die Werte \n"
|
||||
"zugegriffen werden.",
|
||||
|
||||
classifiers=[
|
||||
"License :: OSI Approved :: "
|
||||
"GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules"
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user