Externe DLL Funktion mit Rückgabe in double* Parameter

Python in C/C++ embedden, C-Module, ctypes, Cython, SWIG, SIP etc sind hier richtig.
Antworten
ulihuber
User
Beiträge: 2
Registriert: Samstag 9. Januar 2021, 10:09

Hallo zusammen,

ich bin neu hier, habe in alllen mögliche Sprachen schon was gemacht, in Python aber nicht wirklich der Freak. Im Moment stehe ich vor dem Problem, dass ich für die Ansteuerung spezieller Hardware eine vom Hersteller zur Verfügung stehende DLL einbinden möchte. Dafür gibt es dokumentierte Funktionen und prototypischen Code in C, VB und Delphi. Damit klappt das auch, aber ich möchte nun mal Python.
Die Funktionen sind alle so aufgebaut, dass sie bei Erfolg einen Rückgabewert >0 liefern. Alle Ergebnisse werden in Variablen by Reference zrückgegeben.
Ganz einfach gingen die ersten Schritte mit den Funktionen
Prototyp: BOOL IK220Find (ULONG* pBuffer16);
pBuffer16: Zeiger auf 16 Langworte (16*4 Byte)
ULONG : unsigned 32 Bit
und
Prototyp: BOOL IK220Init (USHORT Axis);
Axis: Nummer der Achse (0 bis 15)
USHORT : unsigned 16 Bit

Code: Alles auswählen

from ctypes import windll,byref, c_ulong
ik220 = windll.LoadLibrary("C:\Windows\System32\IK220DLL.dll")
IKCard = c_ulong()
if  ik220.IK220Find(byref(IKCard)) : print ("Cards found")
for Ax in range(0,3) :
    if ik220.IK220Init(Ax):
        if ik220.IK220Start(Ax):
            print("Start channel {}: OK".format(Ax))
Hier bekomme ich auch brav das Ergebnis:
Cards found
Start channel 0: OK
Start channel 1: OK
Start channel 2: OK
Start channel 3: OK
Und jetzt wird es schwierig. In der nächsten Funktion sollte ich einen Wert über einen Pointer zurückbekommen.
BOOL IK220Read48 (USHORT Axis,
USHORT Latch, double* pData);
double : Fließkomma 64 Bit
Dazu habe ich alles Mögliche probiert, aber vermutlich das Konzept hinter den ctypes nicht richtig verstanden.

Code: Alles auswählen

from ctypes import windll,byref, c_ulong, c_double,c_int,pointer,POINTER
ik220 = windll.LoadLibrary("C:\Windows\System32\IK220DLL.dll")
IKCard = c_ulong()
if  ik220.IK220Find(byref(IKCard)) : print ("Cards found")

for Ax in range(0,7) :
    if ik220.IK220Init(Ax):
        if ik220.IK220Start(Ax):
            print("Start channel {}: OK".format(Ax))
            
count = c_double()
while True :
    Ax = 1
    if ik220.IK220Read48(Ax, 0, byref(count)):
        print (count)
Das Ergebnis sieht jetzt so aus:
Cards found
Start channel 0: OK
Start channel 1: OK
Start channel 2: OK
Start channel 3: OK
c_double(-0.125)
c_double(0.218505859375)
c_double(-0.174072265625)
.
.
.
Der Wert in der Variablen ist wohl da, aber ich komme nicht dran.
Kann mich da jemand auf's Pferd heben?

Danke für jede Unterstützung!
Uli
__deets__
User
Beiträge: 14480
Registriert: Mittwoch 14. Oktober 2015, 14:29

Steht alles in der offiziellen Doku: https://docs.python.org/3/library/ctype ... data-types -

Code: Alles auswählen

>>> i = c_int(42)
>>> print(i)
c_long(42)
>>> print(i.value)
42
>>> i.value = -99
>>> print(i.value)
-99
>>>
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

Wenn pBuffer16 ein Array mit 16 Elementen erwartet, dann solltest Du da nicht nur ein Long übergeben.
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@ulihuber: Ich würde empfehlen für alle Funktionen aus der DLL die Du verwendest in Python Funktionsprototypen erstellen, also die Signatur angeben. Dabei kannst Du den Funktionen dann auch gleich Namen geben, die den Python-Konventionen entsprechen und den `IK220`-Präfix bei allen Funktionen wegbekommen. Den braucht man in Python nicht, weil Python im Gegensatz zu C Module als Namensräume kennt.

An der Stelle kann man dann auch gleich eine Fehlerprüfung einbauen, die den Rückgabewert der Aufrufe in eine Ausnahme umwandelt, damit man nicht jedes mal prüfen muss ob der Aufruf erfolgreich war, oder das irgendwo mal vergisst zu prüfen.

Generell solltest Du Dir mal den Style Guide for Python Code anschauen.

Neben der Anmerkung von Sirius3 zu dem Array, verwendest Du die Werte aus dem Array auch gar nicht‽

Mal als ungetesteter Ansatz:

Code: Alles auswählen

#!/usr/bin/env python3
from ctypes import byref, c_double, c_uint16, c_uint32, POINTER, windll

IK220_DLL = windll.LoadLibrary(R"C:\Windows\System32\IK220DLL.dll")


class Error(RuntimeError):
    pass


def _check_result(result, function, arguments):
    if result == 0:
        raise Error(f"failed to call {function!r} with {arguments!r}")

    return None


def _make_function(name, argtypes):
    function = getattr(IK220_DLL, name)
    function.argtypes = argtypes
    function.restype = c_uint32
    function.errcheck = _check_result
    return function


find = _make_function("IK220Find", [POINTER(c_uint32 * 16)])
init = _make_function("IK220Init", [c_uint16])
read48 = _make_function("IK220Read48", [c_uint16, c_uint16, POINTER(c_double)])


def main():
    addresses = (c_uint32 * 16)()
    find(byref(addresses))
    axis_count = sum(map(bool, addresses))
    if axis_count:
        print(f"Initializing {axis_count} axes:")
        for axis_number, address in enumerate(addresses):
            if address:
                print(f"Axis #{axis_number} at address {address}")
                init(axis_number)
    
        count = c_double()
        while True:
            read48(1, 0, byref(count))
            print(count.value)
    else:
        print("No axes found!")
    


if __name__ == "__main__":
    main()
Das man auf der Python-Seite hier mit C-Typen und Pointern belästigt wird, und mit Funkionen die alle keinen Rückgabewert haben, ist hässlich, darum würde man mindestens für Funktionen die einen Rückgabewert per Pointer haben eine Wrapper-Funktion in Python schreiben, damit das nicht so ekelig zu verwenden ist:

Code: Alles auswählen

_find = _make_function("IK220Find", [POINTER(c_uint32 * 16)])
init_axis = _make_function("IK220Init", [c_uint16])
_read48 = _make_function(
    "IK220Read48", [c_uint16, c_uint16, POINTER(c_double)]
)


def get_axis_addresses():
    addresses = (c_uint32 * 16)()
    _find(byref(addresses))
    return addresses


def read_float(axis_number, latch_number):
    result = c_double()
    _read48(axis_number, latch_number, byref(result))
    return result.value


def main():
    addresses = get_axis_addresses()
    axis_count = sum(map(bool, addresses))
    if axis_count:
        print(f"Initializing {axis_count} axes:")
        for axis_number, address in enumerate(addresses):
            if address:
                print(f"Axis #{axis_number} at address {address}")
                init_axis(axis_number)
    
        while True:
            print(read_float(1, 0))
    else:
        print("No axes found!")
Als nächstes könnte man dann anfangen das ganze etwas sicherer zu machen, beispielsweise in dem man das Initialisieren einer Achse erzwingt in dem man daraus ein Objekt macht auf dem man die Methoden nur aufrufen kann, nach dem ein Exemplar davon erstellt wurde:

Code: Alles auswählen

_find = _make_function("IK220Find", [POINTER(c_uint32 * 16)])
_init = _make_function("IK220Init", [c_uint16])
_read48 = _make_function(
    "IK220Read48", [c_uint16, c_uint16, POINTER(c_double)]
)


def get_axis_addresses():
    addresses = (c_uint32 * 16)()
    _find(byref(addresses))
    return addresses


class Axis:
    def __init__(self, number):
        if not (0 <= number <= 15):
            raise ValueError(f"axis number {number!r} not in 0..15")
        self._as_parameter_ = c_uint16(number)
        _init(self)

    @property
    def number(self):
        return self._as_parameter_.value

    def read_float(self, latch_number):
        if not 0 <= latch_number <= 3:
            raise ValueError(f"latch number {latch_number} not in 0..3")
        result = c_double()
        _read48(self, latch_number, byref(result))
        return result.value


def main():
    addresses = get_axis_addresses()
    axis_count = sum(map(bool, addresses))
    if axis_count:
        print(f"Initializing {axis_count} axes:")
        axes = list()
        for axis_number, address in enumerate(addresses):
            if address:
                print(f"Axis #{axis_number} at address {address}")
                axes.append(Axis(axis_number))

        axis = next(axis for axis in axes if axis.number == 1)
        while True:
            print(axis.read_float(0))
    else:
        print("No axes found!")
Das mit den Klassen möchte man eventuell in ”beide Richtungen” vertiefen, also eine `IK220`-Klasse, die das erzeugen der `Axis`-Objekte übernimmt/überwacht und eventuell auch ”lazy” gestaltet, also das so ein `Axis`-Objekt gecached wird, und eine `Latch`-Klasse die Achsen- und Latchnummer in einem Objekt kapselt. Bei beiden ”Richtungen” könnte es eventuell Sinn machen `__getitem__()` dafür zu implementieren.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
ulihuber
User
Beiträge: 2
Registriert: Samstag 9. Januar 2021, 10:09

Hallo zusammen,

vielen Dank für die Hilfestellungen.
i.value habe ich inzwischen schon gefunden. :-)
Das mit dem pbuffer16 Array hatte ich inzwischen auch behoben, war natürlich doof. ist jetzt Interessanterweise hat IDLE damit kein Problem gehabt. Python nativ natürlich dann schon.

Danke _Blackjack- für die ausführlichen Tips und vor Allem auch die kompletten Beispiele bis hin zum Wrapper. Die werde ich mir die Tage mal vornehmen und in der Richtung weiterarbeiten.
Den Styleguide muss ich mir reinziehen. Ist nicht so einfach, wenn man in den 80er Jahren das Programmieren gelernt hat, als Objekte noch Gegenstände im Regal waren und dann jahrelang nicht in SW unterwegs war und die aktuellen Entwicklungen nicht verfolgt hat.
Die Werte aus dem Array (HW-Adressen der Karten) braucht man übrigens bestenfalls zur Fehlersuche und selbst da ist das fraglich, weil es PCI-Karten sind. Die sind selbst bei den Herstellerbeispielen nicht ausgewertet.

Inzwischendurch die Hinweise überholt, aber hier der gestern doch noch funktionsfähig hingebastelte Code:

Code: Alles auswählen

from ctypes import windll,byref, c_uint8, c_uint16, c_uint32, c_double,c_int,pointer,POINTER
ik220 = windll.LoadLibrary("C:\Windows\System32\IK220DLL.dll")

IKCard = (c_uint32*16)()
EnStatus= c_uint8()
EnType = c_uint8()
EnPeriod= c_uint32()
EnStep = c_uint32()
EnTurns = c_uint8()
EnRefDist = c_uint8()
EnCntDir = c_uint8()
EnAlarm = c_uint8()
ParRead = c_uint32()
count = c_double()

if  ik220.IK220Find(byref(IKCard)) == 0 :
    print ("No cards found!")
else:
    print ("Cards found!")
    for Ax in range(0,4) :
        print('_____________')
        if ik220.IK220Init(Ax) == 0:
            print("Init axis {}: Error".format(Ax))
        else:
            print("Init axis {}: OK".format(Ax))
            if ik220.IK220Start(Ax)== 0:
                print("Start axis {}: Error".format(Ax))
            else:
                print("Start axis {}: OK".format(Ax))
                ParNr = 1
                ParValue = 1
                if ik220.IK220WritePar(Ax,ParNr, ParValue)==0 :
                    print("Write parameter Endat axis {}: Error".format(Ax))
                else:
                    print("Write parameter Endat axis {}: OK".format(Ax))
                    if ik220.IK220ConfigEn(Ax, byref(EnStatus), byref(EnType), byref(EnPeriod), byref(EnStep), byref(EnTurns), byref(EnRefDist), byref(EnCntDir)) == 0 :
                        print("Get Endat config axis {}: Error".format(Ax))
                        print("Status: {}".format(EnStatus.value))
                        print("Type: {}".format(EnType.value))
                        print("Period: {}".format(EnPeriod.value))
                        print("Step: {}".format(EnStep.value))
                        print("Turns: {}".format(EnTurns.value))
                        print("RefDist: {}".format(EnRefDist.value))
                        print("CntDir: {}".format(EnCntDir.value))
                        ParNr = 1
                        ParValue = 0
                        if ik220.IK220WritePar(Ax,ParNr, ParValue)==0 :
                            print("Write parameter 1Vss axis {}: Error".format(Ax))
                    else:
                        print("Get Endat config axis {}: OK".format(Ax))

while True :
    for Ax in range(0,4):
        ik220.IK220Latch(Ax,0)
        ParNr = 1
        if ik220.IK220ReadPar(Ax,ParNr, byref(ParRead))==0 :
            print("Parameter read axis {}: Error".format(Ax))
        else:
            if ParRead.value == 1:
                if ik220.IK220ReadEn(Ax, byref(EnStatus), byref(count),byref(EnAlarm)):
                    pos = count.value * 0.02
                    print ('Endat : {0:6.2f}  '.format(pos), end='')
            else:
                if ik220.IK220Read48(Ax, 0, byref(count)) == 0:
                    print("Read48 axis {}: Error".format(Ax))
                else:
                    pos = count.value * 0.02
                    print ('Inc   : {0:6.2f}  '.format(pos), end='')

        if Ax == 3:
            print ('\r', end='')
Der wird jetzt natürlich nicht so bleiben ;).
Ich habe wegen der unterschiedlichen Plattformen (die DLL stammt ja aus dem letzten Jahrtausend...) jetzt explizite Datentypen verwendet. Dabei musste ich aber feststellen, dass irgendwas mit den Bitlängen nicht stimmig ist.
Eine Funktion
IK220xxxx(USHORT* Rueckgabewert)
mit USHORT : unsigned 16 Bit
sollte man eigentlich IK220xxx(byref(c_uint16 )) aufrufen können. Geht auch, liefert aber einen falschen Rückgabewert, nämlich statt 1 eine C001h. Ruft man mit IK220xxx(byref(c_uint8 )) auf, passt alles. Seltsam...

Wie auch immer, nochmals vielen Dank. Ich bin erstmal beschäftigt die Anregungen zu verarbeiten.
Gruß
Uli
Antworten