Skip to content

Model-View-Controller

MVC en het gebruik van packages

MVC staat voor Model-View-Controller en is een belangrijk, maar wat diffuus concept in software engineering en is vooral van toepassing op gebruikersinterfaces. Het belangrijkste idee is dat een programma zoveel mogelijk wordt opgesplitst in onderdelen. Het model bevat de onderliggende data en concepten van het programma (een database, meetgegevens, berekeningen, etc.); de controller praat met de fysieke omgeving en reageert bijvoorbeeld op invoer van een gebruiker en past het model aan; de view is een weergave van de data uit het model en vormt de gebruikersinterface zelf. Vaak praten alle onderdelen met elkaar, maar een gelaagd model is makkelijker te overzien en dus eenvoudiger te programmeren. In het geval van een natuurkunde-experiment is dit vaak mogelijk. Daarmee krijgt MVC bij ons een andere betekenis dan bijvoorbeeld bij het bouwen van websites. Het gelaagd MVC-model dat wij gaan gebruiken is hieronder weergegeven:

Een gelaagd model-view-controller model

De controllers communiceren met de apparatuur, bevat informatie en berekeningen die apparatuur afhankelijk zijn; het model bevat de meetgegevens, berekeningen over - en de opzet van - het experiment; de view zorgt voor een gebruikersinterface met weergave van de data.

Het scheiden van je programma in deze lagen kan enorm helpen om ervoor te zorgen dat je geen spaghetticode schrijft — ongestructureerde en moeilijk te begrijpen code. Wanneer het drukken op een knop in de code van de grafische omgeving direct commando's stuurt naar de Arduino of dat de code voor het doen van een enkele meting meteen de $x$-as van een grafiek aanpast, sla je lagen over in ons model en knoop je delen van het programma aan elkaar die niet direct iets met elkaar te maken hebben. De knop moet een meting starten, ja, maar hoe dat precies moet is niet de taak van de gebruikersinterface. En de meting zelf moet zich niet bemoeien met welke grafiek er precies getekend wordt. Je zult merken dat het heel lastig wordt om overzicht te houden en later aanpassingen te doen als je alles door elkaar laat lopen. Je zult dan door je hele code moeten zoeken als je óf de aansturing van de Arduino, óf de grafische interface wilt aanpassen. En dus gaan we alles netjes structureren.

De verschillende onderdelen in het model kunnen we voor ons experiment als volgt beschrijven:

View
Het startpunt van je applicatie. Geeft de opdracht om een meting te starten en geeft na afloop de resultaten van de meting weer op het scherm.
Model
De code die het experiment uitvoert door verschillende metingen te doen en instellingen aan te passen, zoals de spanning over de LED. Het model weet hoe het experiment in elkaar zit en dat er bijvoorbeeld een weerstand van 220 Ω aanwezig is. Geeft opdrachten aan de controller.
Controller
De code die via pyvisa praat met de Arduino. Opdrachten worden omgezet in firmwarecommando's en doorgestuurd naar het apparaat.

Het opsplitsen van je programma hoeft niet in één keer! Dit kan stapsgewijs. Je kunt starten met een eenvoudig script — zoals we hierboven gedaan hebben — en dat langzaam uitbreiden. Je begint klein, verdeelt je code in lagen en bouwt vervolgens verder.

Implementeren van MVC

Het opsplitsen van het basisscript.py in MVC gaan we stapsgewijs doen. We gaan een class maken voor de aansturing van de Arduino, deze class valt in de categorie controller.

Pythondaq: open de repository

Open in GitHub Desktop de repository van pythondaq en open de repository in Visual Studio Code. In de volgende opdrachten ga je het basisscript.py uitbreiden en opsplitsen in MVC.

Pythondaq: controller bouwen

Een gebruiker moet het volgende kunnen doen: Je vraagt een lijst met beschikbare poorten op met de functie list_resources(). Wanneer je weet welke poort de Arduino is gebruik je deze poortnaam om een instance aan te maken van de class ArduinoVISADevice. Met deze class kan je met de Arduino te communiceren. Met de method get_identification() vraag je de identificatiestring op om te controleren dat je met het juiste apparaat communiceert. Je gebruikt de method set_output_value() om een waarde van 828 op het uitvoerkanaal 0 te zetten, omdat de LED gaat branden weet je dat het werkt. Je test de spanningsmeters door met de method get_input_value() eerst van kanaal 1 en daarna van kanaal 2 de waarde op te vragen. Je ziet waardes die overeenkomen met je verwachting. Je rekent de waardes om naar spanningen in volt en controleert daarna de method get_input_voltage() om te zien of deze dezelfde waardes terug geeft voor kanaal 1 en 2. Om te controleren of de waarde die je op het uitvoerkanaal gezet hebt nog steeds gelijk is aan wat je hebt ingesteld vraag je deze waarde op met get_output_value().

Pseudo-code

# def list_resources
#    return list of available ports

# class ArduinoVISADevice
#    def init (ask port from user)
        ...
#    def get_identification
#       return identification string of connected device
#
#   def set_output_value
#       set a value on the output channel
#
#   def get_output_value
#       get the value of the output channel
#      
#   def get_input_value
#       get input value from input channel
#
#   def get_input_voltage
#       get input value from input channel in Volt
Testcode:
basisscript.py
# get available ports
print(list_resources())

# create an instance for the Arduino on port "ASRL28::INSTR"
device = ArduinoVISADevice(port="ASRL28::INSTR")

# print identification string
identification = device.get_identification()
print(identification)

# set OUTPUT voltage on channel 0, using ADC values (0 - 1023)
device.set_output_value(value=828)

# measure the voltage on INPUT channel 2 in ADC values (0 - 1023)
ch2_value = device.get_input_value(channel=2)
print(f"{ch2_value=}")

# measure the voltage on INPUT channel 2 in volts (0 - 3.3 V)
ch2_voltage = device.get_input_voltage(channel=2)
print(f"{ch2_voltage=}")

# get the previously set OUTPUT voltage in ADC values (0 - 1023)
ch0_value = device.get_output_value()
print(f"{ch0_value=}")
(ecpc) > python basisscript.py

Checkpunten:

  • list_resources() is een functie die buiten de class staat.
  • De __init__() method verlangt een poortnaam en opent de communicatie met deze poort.
  • Er is een method get_identification() die de identificatiestring teruggeeft.
  • De set_output_value() en get_output_value() communiceren standaard met kanaal 0.
  • Bij get_input_value en get_input_voltage moet een kanaal opgegeven worden.

Projecttraject:

  • Pythondaq: Repository
  • Pythondaq: Start script
  • Pythondaq: Quick 'n dirty meting
  • Pythondaq: CSV
  • Pythondaq: open de repository
  • Pythondaq: Controller bouwen
  • Pythondaq: Controller implementeren
  • Pythondaq: Controller afsplitsen
  • Pythondaq: Model afsplitsen
  • Pythondaq: Onzekerheid

Je hebt nu een werkende controller, maar je gebruikt het nog niet in je experiment.

Pythondaq: Controller implementeren

Je hebt een Python script die hetzelfde doet als in de opdracht quick 'n dirty meting, maar de code is aangepast zodat er gebruik wordt gemaakt van de class ArduinoVISADevice en de bijbehorende methods.

Pseudo-code

# def list_resources
#   ...

# class ArduinoVISADevice
    ...

# set output voltage from 0 to max
    # measure voltages
    # calculate LED voltage
    # calculate LED current

# plot current vs voltage

Checkpunten:

  • In een script staan list_resources(), ArduinoVISADevice() en de code om de LED te laten branden, metingen te doen en het resultaat te laten zien.
  • Wanneer de class ArduinoVISADevice() uit het script wordt geknipt, werkt de quick 'n dirty niet meer.
  • Het script voldoet nog steeds aan de checkpunten van de opdracht quick 'n dirty meting.

Projecttraject:

  • Pythondaq: Repository
  • Pythondaq: Start script
  • Pythondaq: Quick 'n dirty meting
  • Pythondaq: CSV
  • Pythondaq: open de repository
  • Pythondaq: Controller bouwen
  • Pythondaq: Controller implementeren
  • Pythondaq: Controller afsplitsen
  • Pythondaq: Model afsplitsen
  • Pythondaq: Onzekerheid

Als je de vorige opdracht succesvol hebt afgerond maakt het niet meer uit wat de precieze commando's zijn die je naar de hardware moet sturen. Als je de Arduino in de opstelling vervangt voor een ander meetinstrument moet je de class aanpassen, maar kan alle code die met het experiment zelf te maken heeft hetzelfde blijven.

Nu we de controller hebben gemaakt die de Arduino aanstuurt, blijft er nog een stukje code over. Het laatste stuk waar de plot gemaakt kunnen we beschouwen als een view en de rest van de code — waar de metingen worden uitgevoerd en de stroomsterkte $I$ wordt berekend — is een model. We gaan de code nog wat verder opsplitsen om dat duidelijk te maken én onderbrengen in verschillende bestanden — dat is uiteindelijk beter voor het overzicht.

Pythondaq: Controller afsplitsen

Omdat je het basisscript later gaat uitbreiden om het gebruiksvriendelijker te maken ga je alvast overzicht creëren door de verschillende onderdelen in aparte scripts te zetten. Het bestand arduino_device.py bevat de class ArduinoVISADevice en de functie list_resources(). In basisscript.py importeer je de class en de functie uit de module arduino_device.py zodat je ze daar kunt gebruiken.
ECPC
├── pythondaq
           ├── basisscript.py
           ├── arduino_device.py
           └── •••
└── •••

error

Waarschijnlijk krijg je nog een of meerdere errors als je basisscript.py runt. Lees het error bericht goed door, om welk bestand gaat het arduino_device.py of basisscript.py? Wat is er volgens het error bericht niet goed?

Pseudo-code

arduino_device.py
# def list_resources
#   ...

# class ArduinoVISADevice
    ...
basisscript.py
from arduino_device import ArduinoVISADevice, list_resources

# set output voltage from 0 to max
    # measure voltages
    # calculate LED voltage
    # calculate LED current

# plot current vs voltage

Checkpunten:

  • Alle directe communicatie met de Arduino, firmwarecommando's en pyvisacommando's, staan in de controller
  • Runnen van basisscript.py zorgt ervoor dat een meting start
  • Het basisscript voldoet nog steeds aan de checkpunten van de opdracht quick 'n dirty meting.

Projecttraject:

  • Pythondaq: Repository
  • Pythondaq: Start script
  • Pythondaq: Quick 'n dirty meting
  • Pythondaq: CSV
  • Pythondaq: open de repository
  • Pythondaq: Controller bouwen
  • Pythondaq: Controller implementeren
  • Pythondaq: Controller afsplitsen
  • Pythondaq: Model afsplitsen
  • Pythondaq: Onzekerheid
if __name__ == '__main__'

Later wil je de functie list_resources() netjes in het hele model-view-controller systeem vlechten zodat je als gebruiker de lijst kunt opvragen, maar voor nu wil je af en toe even zien aan welke poort de Arduino hangt. Wanneer je het script arduino_device.py runt wordt er een lijst geprint met poorten. Dit gebeurt niet wanneer het bestand basisscript.py wordt gerund.

modules

Nog niet bekend met if __name__ == '__main__'? kijk dan voor meer informatie in de paragraaf modules.

Pseudo-code

arduino_device.py
# def list_resources
#   ...

# class ArduinoVISADevice
    ...

# print list ports if arduino_device.py is the main script 
# print list ports not if arduino_device.py is imported as a module in another script
basisscript.py
from arduino_device import ArduinoVISADevice

# set output voltage from 0 to max
    # measure voltages
    # calculate LED voltage
    # calculate LED current

# plot current vs voltage

Checkpunten:

  • Er wordt een lijst met poorten geprint wanneer arduino_device.py wordt gerund.
  • De lijst wordt niet geprint wanneer basisscript.py wordt gerund.

Pythondaq: Model afsplitsen

Omdat de uitbreidingen om het basisscript gebruiksvriendelijker te maken vooral de view zullen uitbreiden zet je het model en de view ook in aparte bestanden. Wanneer je het bestand view.py runt roept deze in het model de method scan() van de class DiodeExperiment aan welke een meting start. Om gegevens van het naar de Arduino te sturen maakt het model gebruik van de controller. De gegevens die het model terugkrijgt van de Arduino worden volgens de fysische relaties verwerkt tot de benodigde gegevens en doorgestuurd naar de view. De view presenteert de gegevens in een grafiek. Wanneer je in een ander bereik wilt meten pas je in de view het bereik aan, het model gebruikt dit bereik bij het doen van de meting. Let op, we hernoemen basisscript.py naar diode_experiment.py.
ECPC
├── pythondaq
           ├── diode_experiment.py
           ├── arduino_device.py
           ├── view.py
           └── •••
└── •••

Pseudo-code

arduino_device.py
# def list_resources
#   ...

# class ArduinoVISADevice
    ...
diode_experiment.py
from arduino_device import ArduinoVISADevice, list_resources

# class DiodeExperiment
    ...
    def scan # with start and stop
        # set output voltage from 0 to max
            # measure voltages
            # calculate LED voltage
            # calculate LED current
view.py
from diode_experiment import DiodeExperiment

# get current and voltage from scan(start, stop)

# plot current vs voltage

Checkpunten:

  • Alle directe communicatie met de Arduino, firmwarecommando's en pyvisacommando's, staan in de controller
  • Alle communicatie met de controller staan in het model
  • Het model bevat een class DiodeExperiment
  • De view communiceert alleen met het model.
  • Runnen van view.py zorgt ervoor dat een meting start
  • De bestanden diode_experiment.py en view.py voldoen samen nog steeds aan de checkpunten van de opdracht quick 'n dirty meting.
  • De bestanden bevatten alle code die nodig is en niet meer dan dat.

Projecttraject:

  • Pythondaq: Repository
  • Pythondaq: Start script
  • Pythondaq: Quick 'n dirty meting
  • Pythondaq: CSV
  • Pythondaq: open de repository
  • Pythondaq: Controller bouwen
  • Pythondaq: Controller implementeren
  • Pythondaq: Controller afsplitsen
  • Pythondaq: Model afsplitsen
  • Pythondaq: Onzekerheid

Het oorspronkelijke script dat je gebruikte voor je meting is steeds leger geworden. Als het goed is gaat nu (vrijwel) het volledige script alleen maar over het starten van een meting en het weergeven en bewaren van de meetgegevens. In het view script komen verder geen berekeningen voor of details over welk kanaal van de Arduino op welke elektronische component is aangesloten. Ook staat hier niets over welke commando's de Arduino firmware begrijpt. Dit maakt het veel makkelijker om in de vervolghoofdstukken een gebruiksvriendelijke applicatie te ontwikkelen waarmee je snel en eenvoudig metingen kunt doen.

Pythondaq: Onzekerheid

Omdat je never nooit je conclusies gaat baseren op een enkele meetserie ga je de meting herhalen en foutenvlaggen toevoegen. Je moet weer even hard nadenken over hoe je dat bepaalt en hoe je dat in je code gaat verwerken. Daarom pak je pen en papier, stoot je je buurmens aan en samen gaan jullie nadenken over hoe jullie in dit experiment de onzekerheid kunnen bepalen. Daarna kijken jullie naar de opbouw van de code en maken jullie aantekeningen over wat er waar en hoe in de code aangepast moet worden. Je kijkt naar je repository en ziet dat je de nu-nog-werkende-code hebt gecommit vervolgens ga je stap voor stap (commit voor commit) aan de slag om de aanpassingen te maken. Als het klaar is run je view.py met het aantal herhaalmetingen op 3 en ziet in de grafiek foutenvlaggen op de metingen voor stroom en spanningen staan. Je kijkt op het beeldscherm van je buurmens en ziet daar ook foutenvlaggen verschijnen. Met een grijns kijken jullie elkaar aan en geven een high five .

Pseudo-code

arduino_device.py
# def list_resources
#   ...

# class ArduinoVISADevice
    ...
diode_experiment.py
from arduino_device import ArduinoVISADevice, list_resources

# class DiodeExperiment
    ...
    def scan # with start, stop and number of measurements
        # set output voltage from 0 to max
            # measure voltages
            # calculate LED voltage
            # calculate LED current
        # return LED voltage, LED current and errors
view.py
from diode_experiment import DiodeExperiment

# get current and voltage with errors from scan(start, stop, measurements)

# plot current vs voltage with errorbars

Checkpunten:

  • Het aantal herhaalmetingen kan worden aangepast in de view.
  • De onzekerheid wordt in het model op de correcte manier bepaald.
  • De onzekerheid wordt vanuit het model doorgegeven aan de view.
  • In de view wordt de onzekerheid geplot behorende bij de juiste grootheid.

Projecttraject:

  • Pythondaq: Repository
  • Pythondaq: Start script
  • Pythondaq: Quick 'n dirty meting
  • Pythondaq: CSV
  • Pythondaq: open de repository
  • Pythondaq: Controller bouwen
  • Pythondaq: Controller implementeren
  • Pythondaq: Controller afsplitsen
  • Pythondaq: Model afsplitsen
  • Pythondaq: Onzekerheid
User input

De gebruiker moet in de view het script aanpassen om een andere meting te doen. Kun je input() gebruiken om van de gebruiker input te vragen voor de start, stop en aantal metingen?

Error!

Als de gebruiker in de view.py per ongeluk een negatieve startwaarde of negatieve aantal metingen invult gaat het niet goed. Gebruik Exceptions om dergelijke gevallen af te vangen en een duidelijke error af te geven.