##########################################################################################################
# digitaler Multitester zur Spannungsmessung bis +25VDC und Anzeige von Logikpegeln                      #
# für TTL und CMOS                                                                                       #
# Version 1.0; c2023 by Peter Lampe                                                                      #
# Hardware: Raspberry Pi Pico                                                                            #
# als main.py im Wurzelverzeichnis des Raspberry Pi Pico ablegen                                         #
##########################################################################################################

from machine import I2C, Pin, PWM, Timer
from utime import sleep
from oled import Write, GFX, SSD1306_I2C
from oled.fonts import ubuntu_mono_15, ubuntu_mono_20
from ADS1115 import *


#---start def ISR PIN---
def irqPulse(pin):                                  # ISR (externer Interrupt) - wird bei Auftreten eines H-L-Impulses
    global PulseFlag                                # am PulsePin aufgerufen. Flag enthält Puls-Status; GLOBAL!
    PulseFlag=1                                     # wird auf 1 gesetzt, da Puls gefangen
    offPulse.init(period=300, mode=Timer.ONE_SHOT, callback=irqTimer) #Timer reinitialisieren
#---end def---

#---start def ISR TIMER---                          # ISR, Timer-Interrupt) wird nach Ablauf des Timers offTimer aufgerufen.
def irqTimer(dummy):                                # ! die Funktion erwartet zwingend die Eingabe eines Parameters!  
    global PulseFlag                                # Flag enthält Puls-Status; GLOBAL!
    PulseFlag=0                                     # wird auf 0 gesetzt, um Puls wieder zu löschen
#---end def---

#---start def---
def checkPulse():
     global PulseFlag                               # Flag enthält Puls-Status
                                    
     if PulseFlag == 1:                             # Puls aufgetreten?
        showPulse(1)                                # ja, dann Icon im Display ausgeben und
       
     else:
        showPulse(0)                                # nein, dann Icon löschen
#---end def---
                                                    #-------------------------------Achtung!--------------------------------------------------
                                                    # "PulseFlag" muss in ALLEN Funktionen als "GLOBAL" deklariert werden. Damit kann sowohl im
                                                    # Hauptprogramm als auch und in ALLEN entsprechenden Funktionen darauf zugegriffen werden.
                                                    # Im konkreten Fall wäre die Variable in irqPulse, irqTimer, checkPulse und im Hauptprogramm
                                                    # gültig!
                                                    #-----------------------------------------------------------------------------------------
        

#---start def---
def wipeScreen(Mode):                               # Wiper-Modi aufrufen (1-5)
    
    if Mode==1:                                     #---Mode 1---
        for i in range (32):                        # Display von oben nach unten
            gfx.line(0, i+1, 127, i+1, 1)           # weiß füllen
            oled.show()                                 
 
    elif Mode==2:                                   #---Mode 2---
        for i in range (16):                        # mittig beginnend Display nach oben und unten
            gfx.line(0, 16-i, 127, 16-i, 1)         # weiß füllen, dann von oben und unten
            gfx.line(0, 16+i, 127, 16+i, 1)         # wieder schwarz füllen
            oled.show()
        
        for i in range (16):                        
            gfx.line(0, i, 127, i, 0)               
            gfx.line(0, 32-i, 127, 32-i, 0)         
            oled.show()

    elif Mode==3:                                   #---Mode 3---    
        for i in range (32):                        # weiße Linie löscht Display
            gfx.line(0, i, 127, i, 0)               # von oben nach unten
            gfx.line(0, i+1, 127, i+1, 1)
            oled.show()                               

    elif Mode==4:                                   #---Mode 4---
        for i in range (17):                        # zwei weiße Linien löschen das Display
            gfx.line(0, 15-i, 127, 15-i, 1)         # von der Mitte beginnend nach oben
            gfx.line(0, 16-i, 127, 16-i, 0)         # und unten
            gfx.line(0, 16+i, 127, 16+i, 0)
            gfx.line(0, 17+i, 127, 17+i, 1)
            oled.show()                             
 
    elif Mode==5:                                   #---Mode 5---
        for i in range (16):                        # zwei weiße Linien löschen das Display
            gfx.line(0, i, 127, i, 0)               # von oben und unten beginnend
            gfx.line(0, i+1, 127, i+1, 1)           # zur Mitt
            gfx.line(0, 30-i, 127, 30-i, 1)
            gfx.line(0, 31-i, 127, 31-i, 0)
            oled.show()   

    else:
        oled.fill(0)                                # Bei Bereichsüberschreitung erfolgt einfaches
        sleep(0.3)                                  # löschen des Displays mit 0,3s Verzögerung
        return()                                    # und Rücksprung zum Aufruf
    
    oled.fill(0)                                    # Display final löschen und
    return()                                        # Rücksprung zum Aufruf
#---end def----

#---start def---
def letBeep(Frequency,GP):                          # erzeugt Ton mit einer Frequenz an GPIO
    Beep=PWM(Pin(GP))                               # Zuweisung des GPIO
    if Frequency==0:                                # Ton abschalten
        Beep.duty_u16(Frequency)                    # 0=aus
    else:
        Beep.freq(Frequency)                        # Zuweisung der Frequenz in Hz
        Beep.duty_u16(32767)                        # 0=Tastverhältnis 1:1
#---end def----

#---start def---
def showPulse(onMode):                              # Pulsanzeige im Display 0,ungleich 0 (aus, an)
    if onMode:                                      # akzeptiert alle Eingabeparameter als "1"
        onMode=1
    gfx.line(105, 21, 110, 21, onMode)
    gfx.line(110, 21, 110, 31, onMode)
    gfx.line(110, 31, 115, 31, onMode)
    gfx.line(115, 31, 115, 21, onMode)
    gfx.line(115, 21, 120, 21, onMode)
    gfx.line(120, 21, 120, 31, onMode)
    gfx.line(120, 31, 125, 31, onMode)
    oled.show()                                     # auf Display anzeigen
    
def chargePump():                                   #negative Betriebsspannung über Chargepump erzeugen
    cp=PWM(Pin(6))                                  #Chargepump-PWM an GPIO6 initialisieren
    cp.freq(20000)                                  #Frequenz auf 20kHz einstellen
    cp.duty_u16(32767)                              #Duty cycle ist 50%
                                                    #Ub- an Pin 2 des OPV muss jetzt etwa -2,7V betragen

#+++++++++++++++++++++++Initialisierung++++++++++++++++++++++++++++++++++++++++  
address =72                                         # Startadresse ADS1115 72(73 wenn H gejumpert)
SDA = 16                                            # Definition der I²C-Pins
SCL = 17
#--------------------------------------------------------------------------------------------------
#individuell bestimmen und anpassen:
DivFaktor= 1.08                                     # Korrekturfaktor Spannungsteiler; Wert individuell anpassen!!!
Offset=0.035                                        # Nullpunktkorrektur; Wert individuell anpassen!!!
#--------------------------------------------------------------------------------------------------

Sensor = Pin(15, Pin.IN, Pin.PULL_UP)               # Sensorknopf initialisiert, wenn verwendet
PulsePin = Pin(19,Pin.IN,Pin.PULL_UP)               # Pin zur Erfassung von Pulsen über IRQ
Probe= Pin(18,Pin.OUT)                              # Schaltsignal für Optokoppler an GPIO18; 0=aus, 1=an
PulseFlag=0                                         # wurde in ISR als global definiert, hier erst mal "Pulse gelöscht"
PulsePin.irq(trigger=Pin.IRQ_FALLING, handler=irqPulse)             # IRQ-Definition für fallende Flanke an GPIO14
offPulse = Timer(period=10, mode=Timer.ONE_SHOT, callback=irqTimer) # Timer-IRQ definieren,
                                                                    # bei erstem Aufruf nur kurze Zeit vorsehen

i2c=I2C(0, 400000, scl=Pin(SCL), sda=Pin(SDA))      # Initialisierung I²C-Bus
adc = ADS1115(address, i2c=i2c)                     # Initialisierung ADS1115


chargePump()                                        # negative Betriebsspannung für OPV erzeugen

oled = SSD1306_I2C(128, 32, i2c)                    # hier Displaygröße anpassen
gfx = GFX(128, 32, oled.pixel)                      # hier Displaygröße anpassen
write15 = Write(oled, ubuntu_mono_15)               # hier Schriftart und -größe definieren
write20 = Write(oled, ubuntu_mono_20)               # hier Schriftart und - größe definieren



# **************** Messwerterfassung in einer Endlosschleife*******************

Mode=0                                              # Startup-screen anzeigen
write15.text("TEST-STIFT V1.0",7,2)                 # Bezeichnung + Version
write15.text("c2023 Peter Lampe",2,16)              # Copyrights
gfx.rect(0, 0, 126, 32, 1)                          # Rahmen
oled.show()                                         # auf Display anzeigen
sleep(2)                                            # 2s warten
wipeScreen(2)                                       # Bildwechsel zur Spannungsmessung

while True:
    if Sensor.value()==0:                           # wenn Sensorbetätigung erkannt
        wipeScreen(5)                               # Bildschirm säubern
        Mode+=1                                     # nächsten Messmodus einschalten
        
        if Mode>2:                                  # Beschränkung auf 0, 1, 2
            Mode=0                                  # zyklisches durchschalten der Modi
 
#---Spannungsmesser---
    if Mode==0:                                     # Mode 0 = Spannungsmessung
        adc.setConvRate(ADS1115_250_SPS)            # Umsetzrate [U/s] setzen
        adc.setVoltageRange_mV(ADS1115_RANGE_4096)  # "Vollausschlag" [mV] setzen
        adc.setCompareChannels(ADS1115_COMP_1_GND)  # Kanal 1 aktivieren
        Probe.value(0)                              # Optokoppler deaktiviert-->Spannungsmessung
        letBeep(0,1)                                # Ton aus
        Summe=0
        
        for i in range(8):                          # 8 Werte aufsummieren 
            adc.startSingleMeasurement();           # das entspricht etwa dem
            while adc.isBusy():                     # Teilungsverhältnis am
                pass                                # Vorteiler
            
            Voltage = adc.getResult_V()             # Messung abholen
            Summe+=Voltage                        
                    
        Summe = Summe * DivFaktor - Offset          # exakter Mittelwert über 8 Messwerte
        Wert= str("{:.3f}".format(Summe))           # Zahlenwert formatiert als String
        Wert+= "V "                                 # und noch VOLT dran hängen
  
        print ("Spannung ist", Wert)                # auf auf Konsole ausgeben
            
        write15.text("Spannungsmessung",10,0)       # Mess-Mode
        gfx.line(0, 15, 127, 15, 1)                 # Unterstreichung
       
        if Summe >25:                               # Messwert >25
            write20.text("  -----  ",17,16)         # ja, Überlauf usgeben    
        else:        
            write20.text(Wert,25,16)                # nein, dann Wert ausgeben

        oled.show()                                 # alles auf Display anzeigen
    
#---TTL/VTL---
    elif Mode==1:                                   # Mode=1 Logikpegel TTL/3,3V
        adc.setConvRate(ADS1115_64_SPS)             # Umsetzrate [U/s] setzen (64)
        adc.setVoltageRange_mV(ADS1115_RANGE_4096)  # "Vollausschlag" [mV] setzen (4096)
        adc.setCompareChannels(ADS1115_COMP_0_GND)  # Kanal 1 aktivieren
        Probe.value(1)                              # Optokoppler ein-->Pegelmessung
        
        adc.startSingleMeasurement();               # Umsetzung starten
        while adc.isBusy():                          
            pass                                     
            
        Pegel = adc.getResult_mV()                  # Pegelmesswert abholen
        print (Pegel)                               # Rohwert auf Konsole ausgeben
        
        write15.text("Pegel TTL/LVT",18,0)          # Mess-Mode und
        gfx.line(0, 15, 127, 15, 1)                 # Unterstreichung ausgeben
        if Pegel < 1008:                            # wenn L-Pegel (<0,4V); Wert individuell anpassen!!!
            write20.text(" LOW ",45,16)             # Pegel anzeigen
            letBeep(1000,1)                         # L-Ton generieren
        elif Pegel > 2022:                          # wenn H-Pegel (>2,4V); Wert individuell anpassen!!!
            write20.text("HIGH ",45,16)             # Pegel anzeigen
            letBeep(4000,1)                         # H-Ton generieren
        else:                                       # undefinierter Bereich
            write20.text("UNDEF",45,16)             # Pegel anzeigen
            letBeep(0,1)                            # Ton aus
        oled.show()                                 # auf Display anzeigen         
        checkPulse()                                # Puls-Status anzeigen
                              
#---CMOS-5V---
    elif Mode==2:                                   # Mode=2 Logikpegel CMOS 5V
        adc.setConvRate(ADS1115_64_SPS)             # Umsetzrate [U/s] setzen (64)
        adc.setVoltageRange_mV(ADS1115_RANGE_4096)  # "Vollausschlag" [mV] setzen (4096)
        adc.setCompareChannels(ADS1115_COMP_0_GND)  # Kanal 1 aktivieren
        Probe.value(1)                              # Optokoppler ein-->Pegelmessung
       
        adc.startSingleMeasurement();               # Messung starten
        while adc.isBusy():                         
            pass                                     
            
        Pegel = adc.getResult_mV()                  # Pegelmesswert abholen
        print (Pegel)                               # Rohwert auf Konsole ausgeben
        
        write15.text("Pegel CMOS-5V",18,0)          # Mess-Mode und
        gfx.line(0, 15, 127, 15, 1)                 # Unterstreichung anzeigen
        if Pegel < 1058:                            # wenn L-Pegel (<0,5V); Wert individuell anpassen!!!
            write20.text(" LOW ",45,16)             # Pegel anzeigen
            letBeep(1000,1)                         # L-Ton generieren
        elif Pegel > 3047:                          # wenn H-Pegel (>4,44V); Wert individuell anpassen!!!
            write20.text("HIGH ",45,16)             # Pegel anzeigen
            letBeep(4000,1)                         # H-Ton generieren
        else:                                       # undefinierter Bereich
            write20.text("UNDEF",45,16)             # Pegel anzeigen
            letBeep(0,1)                            # Ton aus
        oled.show()                                 # auf Display anzeigen       
        checkPulse()                                # Puls-Status anzeigen