Skip to content

Labcontrol ontwerp en Python codering

Bart Snijder edited this page Jul 8, 2025 · 41 revisions

Intro

Deze pagina bevat aantekeningen over het ontwerp/structurering van code binnen Labcontrol en over het besturen van apparatuur met Python. Een belangrijke keuze in het ontwikkelproces is die voor communicatie via een USB i.p.v. via een netwerk verbinding, omdat TCP/IP communicatie binnen Windesheim snel lastig wordt. USB was toendertijd de 'easy way out'.

Klassendiagram BaseOscilloscope

Labcontrol is ontstaan via het betere 'jatwerk'. Daarbij is er alleen gejat wat ook begrepen werd. Op een gegeven moment werd het teveel een 'zooitje', met name de implementatie van de TDS2002B en SDS12012XE oscilloscopen. Daarom is er een begin gemaakt met het structuren van de Python implementatie van de oscilloscoop. Omdat Labcontrol geschikt wordt gedacht voor gebruik buiten het E-lab alleen, is het belangrijk om de structurering zo algemeen mogelijk te houden. Daarom is de structureren op basis van de buitenkant, zoals bijvoorbeeld een TDS2002B:

afbeelding

Een TDS2002B heeft, display buiten beschouwing latend, 4 secties:

  1. Vertical
  2. Horizontal
  3. Trigger
  4. De rest ('utility').

Die opdeling is terug te vinden in het ontwerp klassendiagram oscilloscopen binnen Labcontrol: afbeelding

Hier kun je het brondocument vinden. Zoals je kunt zien leunt het klassendiagram vooral op twee uitgangspunten:

  1. Het klassendiagram kent (vrijwel) dezelfde indeling als de buitenkant van een gemiddelde oscilloscoop
  2. De opbouw van de code reflecteert daardoor de wijze van gebruik. Als voorbeeld: Een gebruiker stel de timebase van een oscilloscoop in via 'horizontal'. Dat moet een gebruiker van Labcontrol dat dan ook in de code doen.

Factory-like implementatie in Python

V.w.b. de implementatie in Python bestond de voorkeur om iets van factory-achtig patroon te kunnen gebruiken. Eerste pogingen waren mislukt, maar na verloop van tijd en gepruts leer je dat Python iets als classmethod kent (zie link). Via een artikel dat uitlegt wanneer je __new__ zou moeten gebruiken i.p.v. __init__ (zie link), kwam ik op een stackoverflow pagina waar werd verwezen naar pep487. Hier wordt ingegaan op het zogenaamde "autoregistratie van afgeleide klassen". Deze aanpak is als basis gebruikt voor de verdere factory-implementatie

Autoregistratie van sub-klassen

Sinds Python 3.6 kunnen subclasses zich automatisch laten registreren bij de ouderklasse. Op basis van enige testcode is er code geschreven waarbij:

  1. BaseScope de zgn __init_subclass__ implementeert, waarbij subclasses in een lijst worden opgenomen. Subclasses moeten dan wel in het "import pad" staan, anders wordt een gedefinieerde subclasse niet geregistreerd.
  2. De __new__ functie van BaseScope een lijst met geabonneerde objecten afloopt, waarbij
  3. De subclass, via een eigen overridden getDevice methode, controleert of er een match is, op basis van een VISA url of IDN response. Als er een match is, retourneert getDevice het specifieke object van de subclass.

De werkwijze voor auto registratie van Scope subklassen:

  1. Een nieuw toe te voegen besturing voor een oscilloscoop moet geïmplementeerd worden als een subklasse van BaseScope, bijvoorbeeld:
class TekScope(BaseScope):
  1. De implementatie van een oscilloscoop moet de methode getDevice(cls,url) implementeren, bijvoorbeeld:
 @classmethod
    def getDevice(cls, urls, host):
        """
            Tries to get (instantiate) this device, based on matched url or idn response
            This method will ONLY be called by the BaseScope class, to instantiate the proper object during
            creation by the __new__ method of BaseScope.     
        """    
        urlPattern = "USB" #fix for now, TODO: make more robust e.g. able to connect to TCP or serial.
        if host == None:
            for url in urls:
                if urlPattern in url:
                    rm = visa.ResourceManager()
                    mydev = rm.open_resource(url)
                    mydev.timeout = 10000  # ms
                    mydev.read_termination = '\n'
                    mydev.write_termination = '\n'
                    desc = mydev.query("*idn?")
                    if desc.find("TEKTRONIX,TDS") > -1: #Tektronix device found via IDN.
                        cls.visaInstr = mydev
                        return cls        
        else:
            try:
                ip_addr = socket.gethostbyname(host)
                addr = 'TCPIP::'+str(ip_addr)+'::INSTR'
                mydev = rm.open_resource('TCPIP::'+str(ip_addr)+'::INSTR')
                cls.visaInstr = mydev
                return cls
            except socket.gaierror:
                
                return None
        
        return None
  1. Tijdens creatie van BaseScope, wordt __new__ aangeroepen. Deze loopt de lijst met aangemelde (sub) Scope klassen af:
rm = visa.ResourceManager()
devUrls = rm.list_resources()
for scope in cls.scopeList:
  dev = scope.getDevice(devUrls)

Bovenstaande bestaat op het moment alleen nog in een dummy implementatie. Er is een klasse FakeScopie aangemaakt, waarvan de methode getDevice() niks anders doet dan een nieuw FakeScope object te retourneren. Niet nuttig, maar zoals hieronder te zien is, één die wel lijkt te werken: afbeelding

Te zien is dat variabele scope een referentie bevat naar FakeScopie en niet slechts naar een BaseScope object. Daarom is het te verwachten dat je op deze manier nieuw gemaakt BaseScope subclasse (bijvoorbeeld voor Owon, Hantek, Rigol etc) op eenvoudige wijze in labcontrol 'kan hangen'. Het enige punt is of de registratie van nieuwe subklassen ook soort van automatisch gaat. Misschien kan dat door gebruik te maken het __init__.py bestand, dat in elke map van de src boom te vinden is.

Iets over Descriptors

De hier bovengenoemde PEP487 heeft ook invloed op klasse-attributen en descriptoren. Dat laatste is een interessante, maar lastige feature van Python. Die dingen kan je overal voor gebruiken binnen Python, zo zou met descriptoren "getters/setters" functionaliteit kunnen maken, iets wat met decorators ook zou moeten kun, maar wat mij nog niet gelukt is. Gelukkig zijn er verschillende tutorials en blogs over Python Descriptors te vinden. Een goede vind je hier

Iets over Python property() of @property decoration

Op python docs datamodel staat onder de paragraaf over het aanroepen van o.a. properties het volgende afbeelding

Dit past, helaas, bij de ervaringen met properties: als de baseclass al een property geïmplementeerd heeft door er iets van een waarde te retourneren, lukte het bijv. niet meer om via een subclass implementatie i.p.v. de waarde nu een object te retourneren.

Update 17 maart 2025: de pesterij met Python is dat a) er behoorlijke nog aan de taal geknutseld wordt b) dat er van alles en nog wat geschreven wordt en c) ik weet er gewoon te weinig van. Maar dan lees je op https://realpython.com/python-property/#overriding-properties-in-subclasses : afbeelding

Dus zeggen deze gasten dat het wel kan.... AAAAhhhh..... Het realpython artikel is van december 2024 en uitgaande dat deze gasten weten wat ze doen (namelijk hun stuff gestest) zal het wel betekenen dat ik iets ergens anders verprutst heb, of dat schrijver van dit artikel (per ongeluk) een (oudere) versie van Python draait die niet gelijk is aan de versie waarover de Python organisatie boven overschrijft, of ik heb een oude versie van de officiële documentatie onder de neus gehad. Hoe dan ook: 15 maart heb ik alle properties uit de code gegooid. Het werkt nu als een zonnetje.

#Python weetjes Hier staan een aantal python zaken die tijdens het coderingsproces waren weggezakt en die toch wel handig bleken bij de hand te hebben.

Init methode

De init methode initialiseert het object. En je moet dat strikt letterlijk nemen: initten, niet creëren. Als init wordt uitgevoerd, bestaat het object al. Daarom mag de init methode ook niets retourneren, daar zijn ander methoden voor. Init kan je aanroepen met vaste parameters, maar ook met een variabele argumentenlijst:

class MyClass:
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

# Example usage
obj = MyClass(1, 2, a=3, b=4)
print(obj.args)    # Output: (1, 2)
print(obj.kwargs)  # Output: {'a': 3, 'b': 4}

Links naar diverse artikelen.

Descriptoren: -https://python-course.eu/oop/introduction-to-descriptors.php -https://elfi-y.medium.com/python-descriptor-a-thorough-guide-60a915f67aa9

Vragen over het retourneren van objecten d.m.v. methods

introspectie Casts en objecten

Uitgebreid verhaal of decoration in Python

Python documentatie over compound statements

Over naming en binding

Artikel over new: zie hier

Artikel over gebruik class_method als een soort factory, zodat er een object van het juiste type wordt geretourneerd. Die truuk wordt hier uitgelegd; https://pynative.com/python-class-method/ Artikel over het verschil tussen new en init, legt ook uit welke functie eerst wordt aangeroepen.: https://builtin.com/data-science/new-python Artikel over metaclasses: https://www.geeksforgeeks.org/python-metaclass-__new__-method/ Artikel over opties van classes en functies over meerdere bestanden: https://python-forum.io/thread-41515.html

Clone this wiki locally