Hallo zusammen
Mein momentanes Projekt ist ein GPS-Logger basierent auf einem M4-Feather der diverse Hardwaremodule verwendet. Genauer gesagt ein GPS-Modul (Seriell), SD-Karte (SPI), Temp & Barometer (SPI), Nextion-Display (Seriell), Tonausgabe(GPIO).
Mein Code läuft soweit ohne Probleme. Jedoch habe ich strukturelle Probleme festgestellt. Klassen in Python sind wundervoll, aber Hardware, die nur in einer einzelnen Instanz gebunden sein können, bereiten mir (gedankliche) Probleme.
Wie üblich benutzt man für GPS und andere Hardware Treiberklassen die eine direkte Bindung der Hardware erwarten. Soweit so gut. Wenn ich jetzt übergeordnet weitere Klassen habe, die für ihre Logik eigentlich keine Hardware benötigen, aber halt gerne Piepstöne oder Daten von Hardware lesen/schreiben möchten, wie löse ich das elegant? Zum Beispiel möchte ich Logdaten auf die SD-Karte schreiben obwohl diese Klasse keine Hardware dafür deklariert.
Im Moment instanziere ich die geteilten Hardwareklassen im Hauptprogramm und gebe der Prozedur diese Klasse als Parameter mit. Es muss doch noch einen eleganteren Weg geben.
Die ist mein erster Post hier im Forum. Seid bitte gnädig.
Zu meiner Einordung; ich hab noch einen ZX-81 im Keller.
Wie benutzt man Hardwareresourcen in Klassen, die von anderen Klassen verwendet werden sollen?
- sls
- User
- Beiträge: 480
- Registriert: Mittwoch 13. Mai 2015, 23:52
- Wohnort: Country country = new Zealand();
Hallo digixx und willkommen im Forum,
vielleicht teilst du einfach deinen Code hier, dann hat man ein genaues Bild von dem was du erreichen möchtest. (Zumindest das Design / Grundstruktur, weniger die Logik zum Ansprechen der Hardware)
Grundsätzlich finde ich die Idee nicht verkehrt Klassen für die Hardwaremodule zu verwenden (z.B. GPSSerial, SDCardSPI o.ä.) die dann via Komposition in einer Klasse GPSLogger verwendet werden.
Eine andere Idee wäre noch, die einzelnen Methoden der Hardwareklassen direkt anzusprechen, ohne die Klasse selbst als Objekt zu initialisieren und an GPSLogger zu übergeben / dort im Konstruktor zu initialisieren. (Hier kann man dann klären, ob man dafür tatsächlich Klassen benötigt, oder Funktionen ausreichen), also bspw. `SdCardSPI.write()` wenn man irgendwas außerhalb dieser Hardwareklasse auf die SD-Karte schreiben möchte.
Mfg, sls
PS: die ZX-81 kenne ich nur noch aus Erzählungen, ich konnte erst neulich eine C64 aus einem Dachbodenfund ergattern, ich arbeite mich also Stück für Stück zurück.
vielleicht teilst du einfach deinen Code hier, dann hat man ein genaues Bild von dem was du erreichen möchtest. (Zumindest das Design / Grundstruktur, weniger die Logik zum Ansprechen der Hardware)
Grundsätzlich finde ich die Idee nicht verkehrt Klassen für die Hardwaremodule zu verwenden (z.B. GPSSerial, SDCardSPI o.ä.) die dann via Komposition in einer Klasse GPSLogger verwendet werden.
Eine andere Idee wäre noch, die einzelnen Methoden der Hardwareklassen direkt anzusprechen, ohne die Klasse selbst als Objekt zu initialisieren und an GPSLogger zu übergeben / dort im Konstruktor zu initialisieren. (Hier kann man dann klären, ob man dafür tatsächlich Klassen benötigt, oder Funktionen ausreichen), also bspw. `SdCardSPI.write()` wenn man irgendwas außerhalb dieser Hardwareklasse auf die SD-Karte schreiben möchte.
Mfg, sls
PS: die ZX-81 kenne ich nur noch aus Erzählungen, ich konnte erst neulich eine C64 aus einem Dachbodenfund ergattern, ich arbeite mich also Stück für Stück zurück.
When we say computer, we mean the electronic computer.
Dein beschriebenes Verfahren ist da durchaus der richtige Ansatz. Ein Begriff, den du dir wenn du willst ergoogeln kannst, ist "dependency injection". Das ist hochtraberisch fuer explizit Abhaengigkeiten zB als Argumente an Klassen oder Funktionen zu uebergeben. Damit bleibt deren Code generisch, und kann zB mit Mocking aus Testframeworks auch gut getestet werden, ohne das man dabei auf die per Definition nur als Singletons verfuegbare Hardware angewiesen ist. Geschweige denn die dazu bekommt, die Fehlerfaelle nachzustellen.
Vielen Dank für Eure Antworten.
Das mit der "dependency injection" klingt gut.
Ich lege also je Hardware Port eine Klasse an. Für den SPI-Bus hatte ich den Knopf, dass ich die Devices einzeln deklarieren wollte. Also kommen alle Devices in eine einzelne Klasse mit der SPI-Schnittstelle als Master. Diese Klassen instanziere ich in einem von mir sogenannten Modul (eigene Datei, m_SPI.py) welche alle nötigen Funktionen enthält, damit die Businesslogik mit der Hardware kommunizieren kann. Das mache ich bereits für die seriellen Ports (GPS, Nextion).
Nextion Klasse (cl_nextion.py
Nextion Modul m_nextion.py
Hauptroutine (code.py)
Den Code für den SPI werde ich dann ebenfalls posten.
Das mit der "dependency injection" klingt gut.
Ich lege also je Hardware Port eine Klasse an. Für den SPI-Bus hatte ich den Knopf, dass ich die Devices einzeln deklarieren wollte. Also kommen alle Devices in eine einzelne Klasse mit der SPI-Schnittstelle als Master. Diese Klassen instanziere ich in einem von mir sogenannten Modul (eigene Datei, m_SPI.py) welche alle nötigen Funktionen enthält, damit die Businesslogik mit der Hardware kommunizieren kann. Das mache ich bereits für die seriellen Ports (GPS, Nextion).
Nextion Klasse (cl_nextion.py
Code: Alles auswählen
"""
PAGE Main (0)
dimlow.val #0000
dimhigh.val #0004
dimdelay.val #0008
templow.val #0012
temphigh.val #0016
press3h.val #0020
TempHighEn.val #0024
TempLowEn.val #0028
StormEn.val #0032
"""
"""
Data format set_num, set_txt
Header Command Name Delimiter Value Tail
['@'] [set_num/txt] ['Name'] ['$'] ['1000'] ['#']
Data format get_num, get_txt
Header Command Name Tail
['@'] [get_num/txt] ['Name'] ['#']
Data format get_page_data
Header Command Page Part Tail
['@'] [get_page_data] [0..9-A..Z] [0..9-A..Z] ['#']
Data format action_set / action_reset
Header Command Name Tail
['@'] [action_set] ['Name'] ['#']
['@'] [action_reset] ['Name'] ['#']
"""
class nextion:
get_page_data = const(0x70) # Zeichen p
set_num = const(0x4e) # Zeichen N / Nextion sends value to store
get_num = const(0x6e) # Zeichen n
set_txt = const(0x54) # Zeichen T
get_txt = const(0x74) # Zeichen t
action_set = const(0x42) # Zeichen B
action_reset = const(0x62) # Zeichen b
comStateIdle = const(0)
comStateReading = const(1)
comStateExecute = const(2)
comHead = const(0x40) # Zeichen @
comDelimiter = const(0x24) # Zeichen $
comTail = const(0x23) # Zeichen #
def __init__(self, uart):
self._uart = uart
self._comState = comStateIdle
self._comData = []
self._elementDB = []
def __repr__(self):
return "nextion('{}')".format(self._uart.baudrate)
def __str__(self):
return 'not implemented'
def _c2t(self, data):
data_string = ''.join([chr(b) for b in data])
return data_string
def _c2n(self, data):
data_string = ''.join([chr(b) for b in data])
return int(data_string)
def _sendVal(self, name, value):
if isinstance(value, str):
self._send(name + ".txt=" + '"' + value + '"')
else:
if (name == "sys0" or name == "sys1" or name == "sys2"):
self._send(name + "=" + str(value))
else:
self._send(name + ".val=" + str(value))
def _send(self, buf):
for c in buf:
self._uart.write(c.encode())
self._uart.write(b'\xff\xff\xff')
def addElement(self, name, value):
self._elementDB.append({"n": name, "v": value})
def getElement(self, name):
for e in self._elementDB:
if e.get('n') == name:
return e.get('v')
def setElement(self, name, value, refresh = None):
for e in self._elementDB:
if e.get('n') == name:
e['v'] = value
if refresh == True:
self._sendVal(name, value)
def refreshElement(self, name):
self._sendVal(name, self.getElement(name))
def update(self):
result = 0, 0
while self._uart.in_waiting:
SerialChar = self._uart.read(1)
if self._comState == comStateIdle:
if SerialChar[0] == comHead:
self._comData = []
self._comState = comStateReading
elif self._comState == comStateReading:
if SerialChar[0] == comTail:
self._comState = comStateExecute
else:
self._comData.append(SerialChar[0])
if self._comState == comStateExecute:
self._comState = comStateIdle
if len(self._comData) > 1:
if self._comData[0] == get_page_data:
page = chr(self._comData[1])
part = '0'
if len(self._comData) == 3:
part = chr(self._comData[2])
result = page, part
elif self._comData[0] == set_txt:
delimiter = self._comData.index(comDelimiter)
name = self._c2t(self._comData[1:delimiter])
value = self._c2t(self._comData[delimiter+1:])
self.setElement(name, value)
elif self._comData[0] == set_num:
delimiter = self._comData.index(comDelimiter)
name = self._c2t(self._comData[1:delimiter])
value = self._c2t(self._comData[delimiter+1:])
self.setElement(name, value)
elif self._comData[0] == get_txt:
name = self._c2t(self._comData[1:])
self._sendVal(name, self.getElement(name))
elif self._comData[0] == get_num:
name = self._c2t(self._comData[1:])
self._sendVal(name, self.getElement(name))
elif self._comData[0] == action_set:
name = self._c2t(self._comData[1:])
self.setElement(name, 1)
print('action_set:', name)
elif self._comData[0] == action_reset:
name = self._c2t(self._comData[1:])
self.setElement(name, 0)
print('action_reset:', name)
else:
data_str = ''.join([hex(b) for b in self._comData])
print("Unknown request:", data_str)
return result
"""
ct = time.monotonic()
uart = busio.UART(board.TX, board.RX, stop=1, baudrate=115200, timeout=0.1)
myNextion = nextion(uart)
myNextion.addElement('cTemp', '24.5')
myNextion.addElement('cPress', '985.2')
myNextion.addElement('b3', 0)
myNextion.addElement('GPSrec', 0)
while True:
if ct + 0.5 < time.monotonic():
ct = time.monotonic()
print(myNextion.update())
myNextion.setElement('cTemp', '28', True)
myNextion.refreshElement('cPress')
if myNextion.getElement('b3') == 1:
myNextion.setElement('GPSrec', 1, True)
else:
myNextion.setElement('GPSrec', 0, True)
"""
Code: Alles auswählen
import board
import busio
import digitalio
import cl_nextion
nex_uart = busio.UART(board.TX, board.RX, stop=1, baudrate=115200, timeout=0.1)
Nextion = cl_nextion.nextion(nex_uart)
LED = digitalio.DigitalInOut(board.A3)
LED.direction = digitalio.Direction.OUTPUT
LED.value = True
# **** String Elements
# GPS
Nextion.addElement('cDate', '--.--.--')
Nextion.addElement('cTime', '--.--.--')
Nextion.addElement('cLatDeg', '--')
Nextion.addElement('cLatMin', '--.---')
Nextion.addElement('cLatNS', '-')
Nextion.addElement('cLonDeg', '--')
Nextion.addElement('cLonMin', '--.---')
Nextion.addElement('cLonEW', '-')
Nextion.addElement('cSOG', '-')
Nextion.addElement('cCOG', '-')
Nextion.addElement('cSat', '-')
Nextion.addElement('cAlt', '-')
Nextion.addElement('cHDil', '-')
Nextion.addElement('cGH', '-')
# Sensors
Nextion.addElement('cTemp', '-')
Nextion.addElement('cPress', '-')
Nextion.addElement('cPress3h', '-')
Nextion.addElement('p2txt', '-')
# SD Card
Nextion.addElement('SDCfree', '-')
Nextion.addElement('SDCdir', '-')
Nextion.addElement('SDCfiles', '-')
# **** Integer Elements
# internal
Nextion.addElement('sys0', 0)
Nextion.addElement('sys1', 0)
Nextion.addElement('sys2', 0)
# GPS
Nextion.addElement('satfix', 0)
Nextion.addElement('GPSrec', 0)
# Sensors
Nextion.addElement('nTemp', 0)
Nextion.addElement('nPress3h', 0)
# **** Button Elements
Nextion.addElement('b0', 0) # Print Testpage
Nextion.addElement('b3', 0) # GPS recording
# **** Alert Elements
Nextion.addElement('a1', 0) # Temp low alarm
Nextion.addElement('a2', 0) # Temp high alarm
Nextion.addElement('a3', 0) # Storm alarm
Nextion.addElement('r3', 0) # Storm alarm reset
def refreshPageData(page, part):
#print('Page:', page, part)
data_str = "read data.."
if page == "1": # Main screen showing GPS data
Nextion.refreshElement("cTime")
Nextion.refreshElement("cDate")
Nextion.refreshElement("satfix")
Nextion.refreshElement("cLatDeg")
Nextion.refreshElement("cLatMin")
Nextion.refreshElement("cLatNS")
Nextion.refreshElement("cLonDeg")
Nextion.refreshElement("cLonMin")
Nextion.refreshElement("cLonEW")
Nextion.refreshElement("cCOG")
Nextion.refreshElement("cSOG")
Nextion.refreshElement("cTemp")
Nextion.refreshElement("cPress")
Nextion.refreshElement("nTemp")
Nextion.refreshElement("nPress3h")
Nextion.setElement("GPSrec", Nextion.getElement("b3"), True)
elif page == "2": # info screen
if part == "0":
data_str = '{:12}'.format('*** GPS ***') + "\\r"
data_str += '{:12}'.format('Datum:')
data_str += Nextion.getElement("cDate") + "\\r"
data_str += '{:12}'.format('Zeit UTC:')
data_str += Nextion.getElement("cTime") + "\\r"
data_str += '{:12}'.format('Lat:')
data_str += Nextion.getElement("cLatDeg") + "."
data_str += Nextion.getElement("cLatMin") + " "
data_str += Nextion.getElement("cLatNS") + "\\r"
data_str += '{:12}'.format('Lon:')
data_str += Nextion.getElement("cLonDeg") + "."
data_str += Nextion.getElement("cLonMin") + " "
data_str += Nextion.getElement("cLonEW") + "\\r"
data_str += '{:12}'.format('Geschwind.:')
data_str += Nextion.getElement("cSOG") + " kn\\r"
data_str += '{:12}'.format('Kurs:')
data_str += Nextion.getElement("cCOG") + " °\\r"
data_str += '{:12}'.format('Satelliten:')
data_str += Nextion.getElement("cSat") + "\\r"
data_str += '{:12}'.format('Hoehe:')
data_str += Nextion.getElement("cAlt") + " m\\r"
data_str += '{:12}'.format('Fix:')
fix = Nextion.getElement("satfix")
if fix == 1:
data_str += "2D\\r"
elif fix == 2:
data_str += "3D\\r"
else:
data_str += "none\\r"
elif part == "1":
data_str = '{:12}'.format('*** Sensoren ***') + "\\r"
data_str += '{:12}'.format('Temperatur:')
data_str += Nextion.getElement("cTemp") + "°C\\r"
data_str += '{:12}'.format('Luftdruck:')
data_str += Nextion.getElement("cPress") + " hPa\\r"
data_str += '{:12}'.format('Druck / 3h:')
data_str += Nextion.getElement("cPress3h") + " hPa\\r"
elif part == "2":
data_str = '{:12}'.format('*** SD Karte ***') + "\\r"
data_str += '{:12}'.format('Free:')
data_str += Nextion.getElement("SDCfree") + " MB\\r"
Nextion.setElement("p2txt", data_str, True)
elif page == "3":
Nextion.refreshElement("cPress")
Nextion.refreshElement("cPress3h")
Nextion.refreshElement("nTemp")
def update():
page, part = Nextion.update()
if page != 0:
toggleLED() # show page request
refreshPageData(page, part)
def updateClimateGraph(press,temp):
# graph height = 250 px
v = int((float(press) - 950) * 2.0)
Nextion.setElement('sys0', v, True)
v = int((float(temp) * 6.25))
Nextion.setElement('sys1', v, True)
print("ClimateGraph updated")
def toggleLED():
LED.value = not LED.value
Code: Alles auswählen
import board
import busio
import digitalio
import time
import neopixel
import cl_gps
import cl_sensors
import cl_events
import cl_sound
import cl_sdcard
import cl_vars
import m_nextion
import m_io
import m_data
NEOpix = neopixel.NeoPixel(board.NEOPIXEL, 1)
NEOpix[0] = (0, 1, 0)
# Loudspeaker
SPKR = cl_sound.sounds(board.D13)
# GPS Module
gps_uart = busio.UART(board.D12, board.D11, baudrate=9600, timeout=20, receiver_buffer_size=256)
GPS = cl_gps.gps(gps_uart)
# Create the SPI bus for multiple devices
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
# BMP280 Sensor (Temp/Pressure)
bmp_cs = digitalio.DigitalInOut(board.D5)
sensor = cl_sensors.sensor_bmp280(spi, bmp_cs)
sensor.sea_level_pressure = 1013
# SD Card
sd_cs = digitalio.DigitalInOut(board.D9)
sd_cd = digitalio.DigitalInOut(board.D6)
sd_root = '/sd'
SDC = cl_sdcard.sdcard(spi, sd_cs, sd_cd, sd_root)
if SDC.available():
SDC.createDir('/logs')
SDC.createDir('/kml')
SDC.listDir()
SDC.listDir('/logs')
SDC.listDir('/kml')
print("Free:", SDC.getFreeSpace(), "MB")
print('\n')
else:
print('no Card found\n')
# Declare Events
ev500msec = cl_events.events(0.5)
ev1sec = cl_events.events(1)
ev5sec = cl_events.events(5)
ev1min = cl_events.events(60)
ev3min = cl_events.events(180)
# Global vars
GPS_FixStatus = cl_vars.vars("0")
SPKR.PlayPowerOnSound()
# run main loop
while True:
tstart = time.monotonic()
GPS.Update()
m_nextion.update()
tstop = time.monotonic()
if tstop - tstart > 0.5:
print("Update overload")
if ev500msec.isdue:
m_io.doHeartBeat()
m_nextion.Nextion.setElement("satfix", GPS.Fix3D)
m_nextion.Nextion.setElement("cTemp", sensor.Temp)
m_nextion.Nextion.setElement("cPress", sensor.Pressure)
m_nextion.Nextion.setElement("nTemp", sensor.TempHiRes * 10)
m_data.handlerGPSLog(m_nextion.Nextion.getElement("b3"), GPS, SDC)
if GPS.IsValid:
m_nextion.Nextion.setElement("cDate", GPS.Date)
m_nextion.Nextion.setElement("cTime", GPS.Time)
Deg, Min, NS = GPS.Latitude
m_nextion.Nextion.setElement("cLatDeg", Deg)
m_nextion.Nextion.setElement("cLatMin", Min)
m_nextion.Nextion.setElement("cLatNS", NS)
Deg, Min, EW = GPS.Longitude
m_nextion.Nextion.setElement("cLonDeg", Deg)
m_nextion.Nextion.setElement("cLonMin", Min)
m_nextion.Nextion.setElement("cLonEW", EW)
m_nextion.Nextion.setElement("cSOG", GPS.Speed)
m_nextion.Nextion.setElement("cCOG", GPS.Course)
m_nextion.Nextion.setElement("cSat", GPS.Satellites)
m_nextion.Nextion.setElement("cAlt", GPS.Altitude)
m_nextion.Nextion.setElement("cHDil", GPS.HDilution)
m_nextion.Nextion.setElement("cGH", GPS.GeoidHeight)
if m_nextion.Nextion.getElement("b0") == 1:
m_data.printTestpage()
m_nextion.Nextion.setElement("b0", 0)
print("Printer: print Testpage")
if ev1sec.isdue:
if GPS_FixStatus.hasChanged(GPS.Fix > 0):
# SPKR.PlayGPSfix(GPS.Fix)
print("GPS-Fix:", GPS.Fix)
if ev5sec.isdue:
pass #print("5sec Event")
if ev1min.isdue:
# Alarm Temp High
if m_nextion.Nextion.getElement("a1") == 1:
SPKR.Up(2)
m_nextion.Nextion.setElement("a1", 0)
print("Alert: Temp to high")
# Alarm Temp Low
if m_nextion.Nextion.getElement("a2") == 1:
SPKR.Down(2)
m_nextion.Nextion.setElement("a2", 0)
print("Alert: Temp to low")
# Alarm Storm
if m_nextion.Nextion.getElement("r3") == 1:
# Clear PressureData
m_data.clearClimateLog()
m_nextion.Nextion.setElement("a3", 0)
m_nextion.Nextion.setElement("r3", 0)
print("Alert: Storm cleared")
if m_nextion.Nextion.getElement("a3") == 1:
SPKR.HighLow(3)
m_nextion.Nextion.setElement("a3", 0)
print("Alert: Storm")
"""
if GPS.IsValid:
if m_nextion.Nextion.getElement("b3") == 1:
SDC.write2Log(GPS.reverseDate + ".txt", GPS.Date, GPS.Time, GPS.LatitudeLog, GPS.LongitudeLog, GPS.Speed)
"""
if ev3min.isdue:
m_nextion.updateClimateGraph(sensor.Pressure, sensor.Temp)
m_data.updateCLILog(sensor.PressureHiRes, sensor.TempHiRes)
m_nextion.Nextion.setElement("cPress3h", '%0.1f' % m_data.calc3hPressRatio())
m_nextion.Nextion.setElement("nPress3h", int(m_data.calc3hPressRatio() * 10))
m_nextion.Nextion.setElement("SDCfree", '%s' % SDC.getFreeSpace())
Ich weiss nicht genau woher du die const-Dinger hast, aber fuer vieles wie zB deine States sind die Ueberfluessig. Eine Ganzzahl ist schon konstant. Wenn du __str__ nicht implementieren willst, dann mach das einfach nicht. Da einen String zurueck zu geben, IST eine Implementierung. Nur eine unsinnige. Und __str__ delegiert per default auf __repr__, und das hast du ja - warum passt das nicht fuer __str__? Immer noch besser als "not implemented"
Die Namenskonventionen fuer Klassen, Konstanten und Variablen sind auch verwirrend ignoriert: KlassenSchreibtManCamelCase, KONSTANTEN_GROSS_MIT_UNTERSTRICH, variablen_und_funktionen_oder_methoden_klein_mit_unterstrich. Dafuer spart man sich dann sowas wie einen cl_-Praefix.
Diese riesigen if-Kaskaden sind sehr unuebersichtlich. Da bietet sich eine Aufteilung in einzelne Methoden an, und die werden dann nur noch aufgerufen. Wenn man will, kann man das auch so machen
So kann man einfach neue Zustaende hinzufuegen.
Diese riesen String-Stueckelungen macht man auch nicht. Benutz wenn du > python3.6 bist format-string, und wenn du auf einer aelteren Version bist, steck die Werte in ein dict und formatier damit. Und natuerlich den gesamten String als """-Literal.
Namen wie "a2" und "r3" sind mir persoenlich zu magisch, wenn die sich nicht gleich an der Quelle verbessern lassen, sollten sie besse benamte Konstante sein - zB ALARM_KATZENKLAPPE = "a1"
Statt getElement & Co sollte man ueber __getitem__ und __setitem__ nachdenken.
So. Mehr vielleicht spaeter
Die Namenskonventionen fuer Klassen, Konstanten und Variablen sind auch verwirrend ignoriert: KlassenSchreibtManCamelCase, KONSTANTEN_GROSS_MIT_UNTERSTRICH, variablen_und_funktionen_oder_methoden_klein_mit_unterstrich. Dafuer spart man sich dann sowas wie einen cl_-Praefix.
Diese riesigen if-Kaskaden sind sehr unuebersichtlich. Da bietet sich eine Aufteilung in einzelne Methoden an, und die werden dann nur noch aufgerufen. Wenn man will, kann man das auch so machen
Code: Alles auswählen
class Foo:
def tuwas(self, argument):
self.ARGUMENT2AKTION[argument](self)
def argument_a(self):
print("a")
def argument_b(self):
print("b")
ARGUMENT2AKTION = { "a" : argument_a, "b": argument_b}
Diese riesen String-Stueckelungen macht man auch nicht. Benutz wenn du > python3.6 bist format-string, und wenn du auf einer aelteren Version bist, steck die Werte in ein dict und formatier damit. Und natuerlich den gesamten String als """-Literal.
Namen wie "a2" und "r3" sind mir persoenlich zu magisch, wenn die sich nicht gleich an der Quelle verbessern lassen, sollten sie besse benamte Konstante sein - zB ALARM_KATZENKLAPPE = "a1"
Statt getElement & Co sollte man ueber __getitem__ und __setitem__ nachdenken.
So. Mehr vielleicht spaeter

Das der Code nicht Python konform ist schon klar. Da sind halt die Programmier-Erfahrungen von 40 Jahren mit drin 
Aber ich lerne gerne dazu. Das zeigt schön das Problem bei Programmiersprachen. Die Beispiele, Tutorials etc. die man im Internet findet, erwähnen solche Dinge gar nicht.
Das in meinen Klassen noch unnötige Dinge wie __str__ stecken ist darob geschuldet, dass ich zum Verstehen von der Funktionsweise ein paar Tutorials ausprobiert habe. Wird aber noch alles bereinigt.
a2,r3 kommen aus der Kommunikation mit dem Nextion. Die Daten werden per ASCII übertragen deren Elementname möglichst kurz sein sollte. Wegen den fünf wollte ich das noch so lassen bis alles fertig ist. Ich muss sonst das Nextion ebenfalls neu programmieren.
Hier mal zwei Bilder damit Du sehen kannst für was ich den ganzen Aufwand mache




Aber ich lerne gerne dazu. Das zeigt schön das Problem bei Programmiersprachen. Die Beispiele, Tutorials etc. die man im Internet findet, erwähnen solche Dinge gar nicht.
Das in meinen Klassen noch unnötige Dinge wie __str__ stecken ist darob geschuldet, dass ich zum Verstehen von der Funktionsweise ein paar Tutorials ausprobiert habe. Wird aber noch alles bereinigt.
a2,r3 kommen aus der Kommunikation mit dem Nextion. Die Daten werden per ASCII übertragen deren Elementname möglichst kurz sein sollte. Wegen den fünf wollte ich das noch so lassen bis alles fertig ist. Ich muss sonst das Nextion ebenfalls neu programmieren.
Hier mal zwei Bilder damit Du sehen kannst für was ich den ganzen Aufwand mache




Schoenes Projekt.
Und wie gesagt, wenn du das nicht aendern kannst, kannst du immerhin gute Konstanten fuer die magischen Nummern nehmen. Das hilft fremden, deinen Code zu verstehen. Das bin heute ich, aber auch Zukunfts-diggix ist ein Fremder, der es dir danken wird, wenn er nach 3 Wochen Pause mal wieder einen Blick drauf wirft.
Und wie gesagt, wenn du das nicht aendern kannst, kannst du immerhin gute Konstanten fuer die magischen Nummern nehmen. Das hilft fremden, deinen Code zu verstehen. Das bin heute ich, aber auch Zukunfts-diggix ist ein Fremder, der es dir danken wird, wenn er nach 3 Wochen Pause mal wieder einen Blick drauf wirft.
- __blackjack__
- User
- Beiträge: 14052
- Registriert: Samstag 2. Juni 2018, 10:21
- Wohnort: 127.0.0.1
- Kontaktdaten:
@digixx: Namenskonventionen werden aber schon hier und da erwähnt, insbesondere in der Python-Dokumentation im Tutorial, denn PEP8 ist ja wie alle PEPs Teil des Informationsmaterials das direkt von den Python-Entwicklern kommt.
Noch ein paar Anmerkungen zum `cl_nextion`-Modul:
Die `const()`-Geschichten gehören nicht in die Klasse. Das ist sehr verwirrend weil die damit entgegen der normalen Python-Semantik als Konstanten plötzlich überall im Modul verfügbar sind obwohl sie auf Modulebene gar nicht definiert sind.
Literale Zeichenketten sind nicht zum auskommentieren von Code gedacht. Für Kommentare ist ausschliesslich ``#`` da. Jeder vernünftige Editor lässt einen damit auch Blöcke von Code ein- und auskommentieren.
Die `nextion.__repr__()` hält sich nicht an die Konventionen. Entweder ist das etwas was man in den Quelltext kopieren könnte und das wieder ein gleiches Objekt erzeugen würde, oder es ist etwas das in ”spitze Klammern” eingefasst ist, und dort als erstes den Klassennamen enthält. Die vorhandene `__repr__()` gibt da etwas zurück das so aussieht als könnte man es so im Quelltext schreiben, aber die `nextion.__init__()` könnte an der Stelle mit der Baudrate als Argument überhaupt nichts anfangen.
`_c2t()` und `_c2n()` sind keine Methoden sondern einfach nur Funktionen die in die Klasse gesteckt wurden. Entweder sollte man die als Funktionen herausziehen oder entsprechend als `staticmethod` definieren, damit das klarer ist und sich keiner wundert ob das so sein soll.
Bei den beiden Funktionen könnte man die „list comprehension“ zu einem Generatorausdrück machen. Aber selbst das könnte man sich sparen, weil man hier `map()` verwenden kann.
`_c2n()` macht fast das gleiche wie `_c2t()`, das braucht man also nicht in zwei Funktionen schreiben, sondern kann die eine aus der anderen heraus aufrufen. Wobei `_c2n` nirgends verwendet wird‽ Soll das so?
Bei ``if (name == "sys0" or name == "sys1" or name == "sys2"):`` sind die Klammern überflüssig und das kann man besser mit ``in`` ausdrücken: ``if name in ["sys0", "sys1", "sys2"]``.
Bei `_send()` verstehe ich die Schleife nicht wirklich. Man kann doch einfach den Buffer kodieren und senden‽
Die `_elemendDB` und die dazugehörigen Methoden sind schräg. Warum ist das so eine komische Liste mit Wörterbüchern als Elementen die immer genau zwei Schlüssel/Wert-Paare enthalten? Warum wird `dict.get()` verwendet, wo man sich doch sicher sein kann, dass die Schlüssel "n" und "v" immer existieren?
Wenn man mit `add_element()` den gleichen Namen mehr als einmal hinzufügt, dann wird von `get_element()` nur der erste Eintrag berücksichtigt, von `set_element()` aber jeder, also ggf. auch mehr als ein Refresh gesendet. Warum ist `_elemendDB` nicht einfach ein Wörterbuch?
Bei `set_element()` sollte `refresh` entweder `False` oder `True` sein, nicht `None` oder `True`.
Update gibt entweder ein Tupel mit zwei 0en zurück oder ein Tupel mit zwei Zeichenketten. Das ist eine komische API.
Ich würde da auch testen ob der Zustand einen ungültigen Wert angenommen hat und entsprechend reagieren. Sonst macht die Methode ja einfach immer gar nichts.
Die Methode ist für meinen Geschmack auch ein bisschen lang und die macht ja auch zwei Dinge die man ganz gut trennen kann: den Zustand verwalten und empfangene Daten ausführen.
Eine Liste für die empfangenen Bytes zu verwenden ist vielleicht nicht die beste Lösung. `bytearray` wäre hier sicher der passendere Datentyp.
Ungetestet:
Noch ein paar Anmerkungen zum `cl_nextion`-Modul:
Die `const()`-Geschichten gehören nicht in die Klasse. Das ist sehr verwirrend weil die damit entgegen der normalen Python-Semantik als Konstanten plötzlich überall im Modul verfügbar sind obwohl sie auf Modulebene gar nicht definiert sind.
Literale Zeichenketten sind nicht zum auskommentieren von Code gedacht. Für Kommentare ist ausschliesslich ``#`` da. Jeder vernünftige Editor lässt einen damit auch Blöcke von Code ein- und auskommentieren.
Die `nextion.__repr__()` hält sich nicht an die Konventionen. Entweder ist das etwas was man in den Quelltext kopieren könnte und das wieder ein gleiches Objekt erzeugen würde, oder es ist etwas das in ”spitze Klammern” eingefasst ist, und dort als erstes den Klassennamen enthält. Die vorhandene `__repr__()` gibt da etwas zurück das so aussieht als könnte man es so im Quelltext schreiben, aber die `nextion.__init__()` könnte an der Stelle mit der Baudrate als Argument überhaupt nichts anfangen.
`_c2t()` und `_c2n()` sind keine Methoden sondern einfach nur Funktionen die in die Klasse gesteckt wurden. Entweder sollte man die als Funktionen herausziehen oder entsprechend als `staticmethod` definieren, damit das klarer ist und sich keiner wundert ob das so sein soll.
Bei den beiden Funktionen könnte man die „list comprehension“ zu einem Generatorausdrück machen. Aber selbst das könnte man sich sparen, weil man hier `map()` verwenden kann.
`_c2n()` macht fast das gleiche wie `_c2t()`, das braucht man also nicht in zwei Funktionen schreiben, sondern kann die eine aus der anderen heraus aufrufen. Wobei `_c2n` nirgends verwendet wird‽ Soll das so?
Bei ``if (name == "sys0" or name == "sys1" or name == "sys2"):`` sind die Klammern überflüssig und das kann man besser mit ``in`` ausdrücken: ``if name in ["sys0", "sys1", "sys2"]``.
Bei `_send()` verstehe ich die Schleife nicht wirklich. Man kann doch einfach den Buffer kodieren und senden‽
Die `_elemendDB` und die dazugehörigen Methoden sind schräg. Warum ist das so eine komische Liste mit Wörterbüchern als Elementen die immer genau zwei Schlüssel/Wert-Paare enthalten? Warum wird `dict.get()` verwendet, wo man sich doch sicher sein kann, dass die Schlüssel "n" und "v" immer existieren?
Wenn man mit `add_element()` den gleichen Namen mehr als einmal hinzufügt, dann wird von `get_element()` nur der erste Eintrag berücksichtigt, von `set_element()` aber jeder, also ggf. auch mehr als ein Refresh gesendet. Warum ist `_elemendDB` nicht einfach ein Wörterbuch?
Bei `set_element()` sollte `refresh` entweder `False` oder `True` sein, nicht `None` oder `True`.
Update gibt entweder ein Tupel mit zwei 0en zurück oder ein Tupel mit zwei Zeichenketten. Das ist eine komische API.
Ich würde da auch testen ob der Zustand einen ungültigen Wert angenommen hat und entsprechend reagieren. Sonst macht die Methode ja einfach immer gar nichts.
Die Methode ist für meinen Geschmack auch ein bisschen lang und die macht ja auch zwei Dinge die man ganz gut trennen kann: den Zustand verwalten und empfangene Daten ausführen.
Eine Liste für die empfangenen Bytes zu verwenden ist vielleicht nicht die beste Lösung. `bytearray` wäre hier sicher der passendere Datentyp.
Ungetestet:
Code: Alles auswählen
"""
PAGE Main (0)
dimlow.val #0000
dimhigh.val #0004
dimdelay.val #0008
templow.val #0012
temphigh.val #0016
press3h.val #0020
TempHighEn.val #0024
TempLowEn.val #0028
StormEn.val #0032
----
Data format set_num, set_txt
Header Command Name Delimiter Value Tail
['@'] [set_num/txt] ['Name'] ['$'] ['1000'] ['#']
Data format get_num, get_txt
Header Command Name Tail
['@'] [get_num/txt] ['Name'] ['#']
Data format get_page_data
Header Command Page Part Tail
['@'] [get_page_data] [0..9-A..Z] [0..9-A..Z] ['#']
Data format action_set / action_reset
Header Command Name Tail
['@'] [action_set] ['Name'] ['#']
['@'] [action_reset] ['Name'] ['#']
"""
import time
import board
import busio
from micropython import const
GET_PAGE_DATA = const(0x70) # Zeichen p
SET_NUM = const(0x4E) # Zeichen N / Nextion sends value to store
GET_NUM = const(0x6E) # Zeichen n
SET_TXT = const(0x54) # Zeichen T
GET_TXT = const(0x74) # Zeichen t
ACTION_SET = const(0x42) # Zeichen B
ACTION_RESET = const(0x62) # Zeichen b
COM_STATE_IDLE = const(0)
COM_STATE_READING = const(1)
COM_STATE_EXECUTE = const(2)
COM_HEAD = const(0x40) # Zeichen @
COM_DELIMITER = "$"
COM_TAIL = const(0x23) # Zeichen #
class Nextion:
def __init__(self, uart):
self._uart = uart
self._com_state = COM_STATE_IDLE
self._com_data = bytearray()
self._element_db = {}
def __repr__(self):
return "<{0.__class__.__name__} baudrate={0._uart.baudrate!r}>".format(
self
)
def _send(self, buffer):
self._uart.write(buffer.encode())
self._uart.write(b"\xff\xff\xff")
def _send_value(self, name, value):
if isinstance(value, str):
self._send('{}.txt="{}"'.format(name, value))
else:
self._send(
"{}{}={}".format(
name,
"" if name in ["sys0", "sys1", "sys2"] else ".val",
value,
)
)
def add_element(self, name, value):
self._element_db[name] = value
def get_element(self, name):
return self._element_db.get(name)
def set_element(self, name, value, refresh=False):
if name in self._element_db:
self._element_db[name] = value
if refresh:
self._send_value(name, value)
def refresh_element(self, name):
self._send_value(name, self.get_element(name))
def _execute_request(self, page, part):
command = self._com_data[0]
if command == GET_PAGE_DATA:
page = chr(self._com_data[1])
part = "0"
if len(self._com_data) == 3:
part = chr(self._com_data[2])
elif command in [SET_TXT, SET_NUM]:
name, delimiter, value = (
self._com_data[1:].decode("latin-1").partition(COM_DELIMITER)
)
if not delimiter:
raise ValueError(
"delimiter {!r} not found in {!r}".format(
COM_DELIMITER, self._com_data
)
)
self.set_element(name, int(value) if command == SET_NUM else value)
elif command == GET_TXT:
name = self._com_data[1:].decode("latin-1")
self._send_value(name, self.get_element(name))
elif command == GET_NUM:
name = self._com_data[1:].decode("latin-1")
self._send_value(name, self.get_element(name))
elif command == ACTION_SET:
name = self._com_data[1:].decode("latin-1")
self.set_element(name, 1)
print("ACTION_SET:", name)
elif command == ACTION_RESET:
name = self._com_data[1:].decode("latin-1")
self.set_element(name, 0)
print("ACTION_RESET:", name)
else:
print("Unknown request:", self._com_data)
return page, part
def update(self):
page = part = "0"
while self._uart.in_waiting:
byte_value = self._uart.read(1)[0]
if self._com_state == COM_STATE_IDLE and byte_value == COM_HEAD:
self._com_data.clear()
self._com_state = COM_STATE_READING
elif self._com_state == COM_STATE_READING:
if byte_value == COM_TAIL:
self._com_state = COM_STATE_EXECUTE
else:
self._com_data.append(byte_value)
else:
assert False, "unexpeted state {!r}".format(self._com_state)
if self._com_state == COM_STATE_EXECUTE:
self._com_state = COM_STATE_IDLE
if len(self._com_data) > 1:
page, part = self._execute_request(page, part)
return page, part
def main():
current_time = time.monotonic()
uart = busio.UART(board.TX, board.RX, stop=1, baudrate=115200, timeout=0.1)
nextion = Nextion(uart)
nextion.add_element("cTemp", "24.5")
nextion.add_element("cPress", "985.2")
nextion.add_element("b3", 0)
nextion.add_element("GPSrec", 0)
while True:
if current_time + 0.5 < time.monotonic():
current_time = time.monotonic()
print(nextion.update())
nextion.set_element("cTemp", "28", True)
nextion.refresh_element("cPress")
nextion.set_element(
"GPSrec", 1 if nextion.get_element("b3") == 1 else 0, True
)
if __name__ == "__main__":
main()
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Das mit dem const() kommt aus der Embedded Programierung wo dann die Werte im Flash und nicht im RAM gehalten werden. Normalerweise verwende ich kleine Atmels die gerade mal 64Byte RAM haben. Habe das auch als Beispiel in einem Python-Code gesehen.
Ok. da sind also folgende Baustellen im Code:
a) Falsche Namenskonventionen
b) Python untypische Strukturen
c) Falsche Verwendung von Klassen, Methoden und Funktionen.
Ich verwende Python erstmals in diesem Projekt in grösserem Umfang. Den Code umzustellen braucht jetzt etwas Zeit. Ich muss allerdings noch erwähnen, dass das CircuitPython ist und nicht ein vollwertiges Python 3.x
Die Gründe warum ich CircuitPython verwende, liegt in der Einfachheit für Updates. Das Modul erscheint am PC als 2MB grosser USB-Speicher, den man einfach mit den Textdateien befüllen kann. So kann der User auch selber etwaige Updates machen ohne einen Programmer zu benötigen. Ein riesen Nachteil ist aber das Python nur interpretiert wird und ein Syntaxfehler erst bei CodeExecution bemerkt wird.
Ich bin aber für weitere Tipps dankbar. Wenn ihr noch etwas Futter wollt, kann ich auch den ganzen Quelltext zum herunterladen anbieten
Ok. da sind also folgende Baustellen im Code:
a) Falsche Namenskonventionen
b) Python untypische Strukturen
c) Falsche Verwendung von Klassen, Methoden und Funktionen.
Ich verwende Python erstmals in diesem Projekt in grösserem Umfang. Den Code umzustellen braucht jetzt etwas Zeit. Ich muss allerdings noch erwähnen, dass das CircuitPython ist und nicht ein vollwertiges Python 3.x
Die Gründe warum ich CircuitPython verwende, liegt in der Einfachheit für Updates. Das Modul erscheint am PC als 2MB grosser USB-Speicher, den man einfach mit den Textdateien befüllen kann. So kann der User auch selber etwaige Updates machen ohne einen Programmer zu benötigen. Ein riesen Nachteil ist aber das Python nur interpretiert wird und ein Syntaxfehler erst bei CodeExecution bemerkt wird.
Ich bin aber für weitere Tipps dankbar. Wenn ihr noch etwas Futter wollt, kann ich auch den ganzen Quelltext zum herunterladen anbieten

- __blackjack__
- User
- Beiträge: 14052
- Registriert: Samstag 2. Juni 2018, 10:21
- Wohnort: 127.0.0.1
- Kontaktdaten:
@digixx: Das mit dem `const()` kannst Du ja machen, aber wie gesagt, das verändert die normale Python-Semantik wenn man es nicht auf Modulebene schreibt.
Weiter zu `m_nextion`:
Auf Modulebene gehört nur Code der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Daraus folgt auch das Funktionen und Methoden alles was sie ausser Konstanten benötigen als Argument(e) übergeben bekommen. Und das man Module importieren kann/können muss ohne das etwas ”passiert”. Man sollte in der Regel jedes Modul importieren können, ohne das da mehr passiert als die Definition von Konstanten, Funktionen, und Klassen.
`data_str` wird in der Refresh-Funktion zu früh definiert. Das wird ja nur in einem der Zeige der Funktion dann am Ende auch tatsächlich verwendet, aber grundsätzlich am Anfang definiert. So etwas über den Code zu verteilen macht es aufwändiger/fehleranfälliger wenn man diesen Zweig irgendwann einmal in eine eigene Funktion heraus ziehen möchte.
Hier sieht man dann auch das die API von `Nextion.update()` die mal ein Tupel mit Zahlen und mal ein Tupel mit Zeichenketten liefert, verwirrend ist. Da wird der Fall (0, 0) beziehungsweise ja eigentlich (0, irgendwas) unterschieden vom Fall ("0", "0") in dem die Zahl 0 für kein Ergebnis steht und zwei Zeichenketten eigentlich für Zahlen. Das sollte eher `None` oder (None, None) für kein Ergebnis sein, und tatsächliche Zahlen für `page` und `part`, was ja eigentlich Zahlen sind und keine Zeichenketten.
Zwischenstand (ungeteset):
Der Modulname ist IMHO nicht passend. `nextion` heisst ja auch schon das Modul mit der Klasse die ganz allgemein für das Nextion-Gerät zuständig ist. Dieses Modul ist ja schon näher am konkreten Programm. `gps_logger_ui` vielleicht‽ Und das Modul könnte davon profitieren wenn man add|set|get_element über `__setitem__()` und `__getitem__()` auf `Nextion` implementiert, weil man dass dann auch bei `format()` in den Platzhaltern verwenden kann.
Weiter zu `m_nextion`:
Auf Modulebene gehört nur Code der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Daraus folgt auch das Funktionen und Methoden alles was sie ausser Konstanten benötigen als Argument(e) übergeben bekommen. Und das man Module importieren kann/können muss ohne das etwas ”passiert”. Man sollte in der Regel jedes Modul importieren können, ohne das da mehr passiert als die Definition von Konstanten, Funktionen, und Klassen.
`data_str` wird in der Refresh-Funktion zu früh definiert. Das wird ja nur in einem der Zeige der Funktion dann am Ende auch tatsächlich verwendet, aber grundsätzlich am Anfang definiert. So etwas über den Code zu verteilen macht es aufwändiger/fehleranfälliger wenn man diesen Zweig irgendwann einmal in eine eigene Funktion heraus ziehen möchte.
Hier sieht man dann auch das die API von `Nextion.update()` die mal ein Tupel mit Zahlen und mal ein Tupel mit Zeichenketten liefert, verwirrend ist. Da wird der Fall (0, 0) beziehungsweise ja eigentlich (0, irgendwas) unterschieden vom Fall ("0", "0") in dem die Zahl 0 für kein Ergebnis steht und zwei Zeichenketten eigentlich für Zahlen. Das sollte eher `None` oder (None, None) für kein Ergebnis sein, und tatsächliche Zahlen für `page` und `part`, was ja eigentlich Zahlen sind und keine Zeichenketten.
Zwischenstand (ungeteset):
Code: Alles auswählen
import board
import busio
import digitalio
from nextion import Nextion
def create_led():
led = digitalio.DigitalInOut(board.A3)
led.direction = digitalio.Direction.OUTPUT
led.value = True
return led
def toggle_led(led):
led.value = not led.value
def create_nextion():
uart = busio.UART(board.TX, board.RX, stop=1, baudrate=115200, timeout=0.1)
nextion = Nextion(uart)
# **** String Elements
# GPS
nextion.add_element("cDate", "--.--.--")
nextion.add_element("cTime", "--.--.--")
nextion.add_element("cLatDeg", "--")
nextion.add_element("cLatMin", "--.---")
nextion.add_element("cLatNS", "-")
nextion.add_element("cLonDeg", "--")
nextion.add_element("cLonMin", "--.---")
nextion.add_element("cLonEW", "-")
nextion.add_element("cSOG", "-")
nextion.add_element("cCOG", "-")
nextion.add_element("cSat", "-")
nextion.add_element("cAlt", "-")
nextion.add_element("cHDil", "-")
nextion.add_element("cGH", "-")
# Sensors
nextion.add_element("cTemp", "-")
nextion.add_element("cPress", "-")
nextion.add_element("cPress3h", "-")
nextion.add_element("p2txt", "-")
# SD Card
nextion.add_element("SDCfree", "-")
nextion.add_element("SDCdir", "-")
nextion.add_element("SDCfiles", "-")
# **** Integer Elements
# internal
nextion.add_element("sys0", 0)
nextion.add_element("sys1", 0)
nextion.add_element("sys2", 0)
# GPS
nextion.add_element("satfix", 0)
nextion.add_element("GPSrec", 0)
# Sensors
nextion.add_element("nTemp", 0)
nextion.add_element("nPress3h", 0)
# **** Button Elements
nextion.add_element("b0", 0) # Print Testpage
nextion.add_element("b3", 0) # GPS recording
# **** Alert Elements
nextion.add_element("a1", 0) # Temp low alarm
nextion.add_element("a2", 0) # Temp high alarm
nextion.add_element("a3", 0) # Storm alarm
nextion.add_element("r3", 0) # Storm alarm reset
return nextion
def refresh_page_data(nextion, page, part):
# print("Page:", page, part)
if page == 1: # Main screen showing GPS data
nextion.refresh_element("cTime")
nextion.refresh_element("cDate")
nextion.refresh_element("satfix")
nextion.refresh_element("cLatDeg")
nextion.refresh_element("cLatMin")
nextion.refresh_element("cLatNS")
nextion.refresh_element("cLonDeg")
nextion.refresh_element("cLonMin")
nextion.refresh_element("cLonEW")
nextion.refresh_element("cCOG")
nextion.refresh_element("cSOG")
nextion.refresh_element("cTemp")
nextion.refresh_element("cPress")
nextion.refresh_element("nTemp")
nextion.refresh_element("nPress3h")
nextion.set_element("GPSrec", nextion.get_element("b3"), True)
elif page == 2: # info screen
if part == 0:
text = (
"*** GPS ***\\r"
"Datum: {}\\r"
"Zeit UTC: {}\\r"
"Lat: {}.{} {}\\r"
"Lon: {}.{} {}\\r"
"Geschwind.:{} kn\\r"
"Kurs: {} °\\r"
"Satelliten:{}\\r"
"Hoehe: {} m\\r"
"Fix: {}\\r"
).format(
nextion.get_element("cDate"),
nextion.get_element("cTime"),
nextion.get_element("cLatDeg"),
nextion.get_element("cLatMin"),
nextion.get_element("cLatNS"),
nextion.get_element("cLonDeg"),
nextion.get_element("cLonMin"),
nextion.get_element("cLonEW"),
nextion.get_element("cSOG"),
nextion.get_element("cCOG"),
nextion.get_element("cSat"),
nextion.get_element("cAlt"),
{1: "2D", 2: "3D"}.get(nextion.get_element("satfix"), "none"),
)
elif part == 1:
text = (
"*** Sensoren ***\\r"
"Temperatur:{}°C\\r"
"Luftdruck: {} hPa\\r"
"Druck / 3h:{} hPa\\r"
).format(
nextion.get_element("cTemp"),
nextion.get_element("cPress"),
nextion.get_element("cPress3h"),
)
elif part == 2:
text = ("*** SD Karte ***\\rFree: {} MB\\r").format(
nextion.get_element("SDCfree")
)
else:
text = "read data..."
nextion.set_element("p2txt", text, True)
elif page == 3:
nextion.refresh_element("cPress")
nextion.refresh_element("cPress3h")
nextion.refresh_element("nTemp")
def update(nextion, led):
page, part = nextion.update()
if page is not None:
toggle_led(led)
refresh_page_data(nextion, page, part)
def update_climate_graph(nextion, pressure, temperature):
# graph height = 250 px
value = int((float(pressure) - 950) * 2.0)
nextion.set_element("sys0", value, True)
value = int((float(temperature) * 6.25))
nextion.set_element("sys1", value, True)
print("ClimateGraph updated")
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
@__blackjack__: Vielen Dank für die Beispiele. Sieht so natürlich viel aufgeräumter aus. Ich werde das entsprechend umsetzen. Dauert halt ein bisschen.
Was ich noch gesehen habe; Bei der Deklaration einer Konstanten mit constname = const(0) ist diese in den Funktionen verfügbar, sonst nicht. Wie kann ich also Werte definieren, die innerhalb eines Moduls global sind? Gleiches brauche ich auch für die State-Machine in der Funktion update(). Dafür habe ich mir eine Klasse angelegt, mit der ich Variablen erzeugen und mittels Attributen setzen und lesen kann.
Lasst die Änderungen beginnen......
Was ich noch gesehen habe; Bei der Deklaration einer Konstanten mit constname = const(0) ist diese in den Funktionen verfügbar, sonst nicht. Wie kann ich also Werte definieren, die innerhalb eines Moduls global sind? Gleiches brauche ich auch für die State-Machine in der Funktion update(). Dafür habe ich mir eine Klasse angelegt, mit der ich Variablen erzeugen und mittels Attributen setzen und lesen kann.
Code: Alles auswählen
class vars():
def __init__(self, v):
self._value = v
@property
def value(self):
return self._value
@value.setter
def value(self, v):
self._value = v
def hasChanged(self, nv):
if self._value != nv:
self._lastvalue = self._value
self._value = nv
return True
else:
return False
@digixx: ich würde da noch nicht in Properties einsteigen. Wenn Du eine Klasse haben möchtest, welche die letzten Werte speichert, würde ich diese über eine Liste (hier deque) anlegen:
PS.: Der ZX81 kam als Bausatz und wollte zusammen gelötet werden. Er liegt aber nicht mehr im Keller 
Code: Alles auswählen
from collections import deque
class Values:
def __init__(self, value, maxlen=10):
self.values = deque([value], maxlen=maxlen)
def add(self, value):
self.values.append(value)
def get_last_value(self):
return self.values[-1]
def has_changed(self, new_value):
return self.get_last_value() != new_value
def add_if_changed(self, new_value):
"""Returns a boolean whether the new_value was added."""
if self.has_changed(new_value):
self.add(new_value)
return True
return False

Nochmals vielen Dank an alle für Eure Tipps und Beispiele.
Der Code ist jetzt soweit bereinigt das die Namenskonventionen eingehalten werden. Ebenso konnte ich das Problem mit der Hardware lösen. Ich verwende jetzt ein Modul das alle Hardware per create_hardwarename() als Objekt zur Verfügung stellt. Danke __blackjack__ für Deine Vorarbeit.
Gewisse Dinge stehen noch aus. Zum Beispiel das mit dem Dictionary. Ich verwende zwei Arten von Dictionaries. Das eine um die Nextionelemente zu definieren, das andere um Messwerte der Sensoren und GPS Daten zu speichern.
Das der Code zum Teil so ineffizient geschrieben wurde liegt auch daran, dass das debuggen halt einfacher geht, wenn man zuerst einen String generiert den man auch print(en) kann. Oder man sendet jedes Zeichen einzeln weil man noch nicht weiss ob das Display alle Zeichen in dieser Geschwindigkeit einlesen kann. Gerade die Library für das GPS hatte auch noch Fehler. Neben einem Typo wurde in der dazugehörigen Demo der Empfangsbuffer der UART zu klein gewählt. Das hat dazu geführt ,dass alle paar Sekunden die Daten abgeschnitten und somit ungültig waren.
Der Code ist jetzt soweit bereinigt das die Namenskonventionen eingehalten werden. Ebenso konnte ich das Problem mit der Hardware lösen. Ich verwende jetzt ein Modul das alle Hardware per create_hardwarename() als Objekt zur Verfügung stellt. Danke __blackjack__ für Deine Vorarbeit.
Code: Alles auswählen
import os
import board
import busio
import digitalio
import neopixel
from sdcard import SDCard
from sounds import Sounds
from sensors import Sensor_BMP280
from gps import GPS
from adafruit_thermal_printer import ThermalPrinter
heartbeat = digitalio.DigitalInOut(board.A2)
heartbeat.direction = digitalio.Direction.OUTPUT
heartbeat.value = True
NEOpix = neopixel.NeoPixel(board.NEOPIXEL, 1)
# set color to green
NEOpix[0] = (0, 1, 0)
# Create the SPI bus for multiple devices
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
def do_heart_beat():
# toggle LED
heartbeat.value = not heartbeat.value
def create_sensors():
bmp_cs = digitalio.DigitalInOut(board.D5)
sensors = Sensor_BMP280(spi, bmp_cs)
sensors.sea_level_pressure = 1013
return sensors
def create_sdcard():
sd_cs = digitalio.DigitalInOut(board.D9)
sd_cd = digitalio.DigitalInOut(board.D6)
sd_root = '/sd'
sdc = SDCard(spi, sd_cs, sd_cd, sd_root)
return sdc
def create_gps():
gps_uart = busio.UART(board.D12, board.D11, baudrate=9600, timeout=20, receiver_buffer_size=256)
gps = GPS(gps_uart)
return gps
def create_thermalprinter():
printer_uart = busio.UART(board.SDA, board.SCL, baudrate=19200, timeout=20)
printer = ThermalPrinter(printer_uart)
return printer
def create_speaker():
spkr = Sounds(board.D13)
return spkr
Das Nextion läuft eigenständig, dass heisst nicht der Controller steuert die Anzeige sondern umgekehrt. Das Display ist programmierbar und seine Seiten, Anzeigen, Knöpfe laufen als Objekte mit Events und Skripten darin. Wenn das Display zum Beispiel die Hauptseite anzeigt, sendet es jede Sekunde das Kommando $p1# zum Controller. Dieser sendet dann die entsprechenden Werte für die Elemente der Page zurück. Jetzt kann es den Fall geben, das das Display Werte direkt für ein spezifisches Element anfordert. Ist das nicht im Dictionary enthalten, wirft der Controller eine Exception. Darum die Abfrage mit dict.get()Die `_elemendDB` und die dazugehörigen Methoden sind schräg. Warum ist das so eine komische Liste mit Wörterbüchern als Elementen die immer genau zwei Schlüssel/Wert-Paare enthalten? Warum wird `dict.get()` verwendet, wo man sich doch sicher sein kann, dass die Schlüssel "n" und "v" immer existieren?
Das der Code zum Teil so ineffizient geschrieben wurde liegt auch daran, dass das debuggen halt einfacher geht, wenn man zuerst einen String generiert den man auch print(en) kann. Oder man sendet jedes Zeichen einzeln weil man noch nicht weiss ob das Display alle Zeichen in dieser Geschwindigkeit einlesen kann. Gerade die Library für das GPS hatte auch noch Fehler. Neben einem Typo wurde in der dazugehörigen Demo der Empfangsbuffer der UART zu klein gewählt. Das hat dazu geführt ,dass alle paar Sekunden die Daten abgeschnitten und somit ungültig waren.
- __blackjack__
- User
- Beiträge: 14052
- Registriert: Samstag 2. Juni 2018, 10:21
- Wohnort: 127.0.0.1
- Kontaktdaten:
@digixx: Deine Erklärung zu `dict.get()` verstehe ich nicht. Es geht doch um diesen Code:
Es wird nur an einer einzigen Stelle etwas zu `self._elementDB` hinzugefügt, nämlich in `addElement()`, und da sind garantiert genau die beiden Schlüssel "n" und "v" in jedem Wörterbuch in der Liste. Also kann man auch gefahrlos in den anderen Methoden auf diese beiden Schlüssel zugreifen, ohne Angst vor einem `KeyError` haben zu müssen, denn diese Schlüssel existieren *garantiert*:
Aber sich ein Wörterbuch mit einer Liste in langsam selbst zu basteln, und dann auch noch Wörterbücher dazu zu verwenden ist sowieso keine gute Idee. Wenn man sich so etwas ohne Wörterbücher basteln möchte ist der klassische Weg aus der funktionalen Programmierung eine Liste von Paaren.
Ich würde da auch `add_element()` und `set_element()` nicht trennen und bei `set_element()` das Schlüssel/Wert-Paar einfach hinzufügen falls es noch nicht existiert. Alternativ sollte man bei `add_element()` und `set_element()` auch explizit mit einer Ausnahme reagieren wenn der Benutzer etwas verbotenes tut, also beispielsweise ein Element mehr als einmal per `add_element()` anlegen oder einen Wert mit `set_element()` für einen nicht vorhandenen Namen setzen. Beide Fehler rutschen bei Deinem Code einfach durch, und mehrfaches hinzufügen kann zu Folgefehlern bei `set_element()` mit `refresh` führen. Zumindest bei der Implementierung von oben.
Code: Alles auswählen
class nextion:
def __init__(self, uart):
...
self._elementDB = []
...
def addElement(self, name, value):
self._elementDB.append({"n": name, "v": value})
def getElement(self, name):
for e in self._elementDB:
if e.get("n") == name:
return e.get("v")
def setElement(self, name, value, refresh=None):
for e in self._elementDB:
if e.get("n") == name:
e["v"] = value
if refresh == True:
self._sendVal(name, value)
...
Code: Alles auswählen
class nextion:
def __init__(self, uart):
...
self._elementDB = []
...
def addElement(self, name, value):
self._elementDB.append({"n": name, "v": value})
def getElement(self, name):
for e in self._elementDB:
if e["n"] == name:
return e["v"]
def setElement(self, name, value, refresh=None):
for e in self._elementDB:
if e["n"] == name:
e["v"] = value
if refresh == True:
self._sendVal(name, value)
...
Ich würde da auch `add_element()` und `set_element()` nicht trennen und bei `set_element()` das Schlüssel/Wert-Paar einfach hinzufügen falls es noch nicht existiert. Alternativ sollte man bei `add_element()` und `set_element()` auch explizit mit einer Ausnahme reagieren wenn der Benutzer etwas verbotenes tut, also beispielsweise ein Element mehr als einmal per `add_element()` anlegen oder einen Wert mit `set_element()` für einen nicht vorhandenen Namen setzen. Beide Fehler rutschen bei Deinem Code einfach durch, und mehrfaches hinzufügen kann zu Folgefehlern bei `set_element()` mit `refresh` führen. Zumindest bei der Implementierung von oben.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
@__blackjack__
Sicher ist das sehr unelegant gelöst, aber es funktioniert. Ich programmiere in Python ja erst seit ein paar Wochen.
Das Konzept für dieses Modul lautet wir folgt:
- Es sollen alle Anzeigeelemente welche Daten vom Controller benötigen oder diesem zugestellt werden in einem Dict angelegt werden. [add_element()]
- Das Display und der Controller greifen über set_element(), get_element() darauf zu.
- Sind Elemente mit diesem Namen nicht bekannt, wird die Anfrage ingoriert (Erklärung siehe unten)
Es könnte vorkommen, dass der Controller wegen Übertragungsfehler einen falschen Elementnamen im Kommando empfängt. Es gibt keine Prüfsumme. Wird jetzt ein Element mit diesem unbekannten Namen abgerufen, gibt es eine Exception und der Controller bricht ab. Das heisst das Log und die gespeicherten Wegpunkte sind verloren. Das Gerät muss aus -und eingeschalten werden. Ist ein Name im Dict nicht vorhanden soll die Abfrage / Setzen des Wertes ignoriert werden. Es ist nur eine Anzeige. Das ist auch der Entwicklung geschuldet, da ich nicht wegem jeden neuen Display-Element das Gegenstück im Controller einpflegen will.
Aber Du hast recht. Ich werde mir eine elegantere Lösung überlegen.
Sicher ist das sehr unelegant gelöst, aber es funktioniert. Ich programmiere in Python ja erst seit ein paar Wochen.
Das Konzept für dieses Modul lautet wir folgt:
- Es sollen alle Anzeigeelemente welche Daten vom Controller benötigen oder diesem zugestellt werden in einem Dict angelegt werden. [add_element()]
- Das Display und der Controller greifen über set_element(), get_element() darauf zu.
- Sind Elemente mit diesem Namen nicht bekannt, wird die Anfrage ingoriert (Erklärung siehe unten)
Es könnte vorkommen, dass der Controller wegen Übertragungsfehler einen falschen Elementnamen im Kommando empfängt. Es gibt keine Prüfsumme. Wird jetzt ein Element mit diesem unbekannten Namen abgerufen, gibt es eine Exception und der Controller bricht ab. Das heisst das Log und die gespeicherten Wegpunkte sind verloren. Das Gerät muss aus -und eingeschalten werden. Ist ein Name im Dict nicht vorhanden soll die Abfrage / Setzen des Wertes ignoriert werden. Es ist nur eine Anzeige. Das ist auch der Entwicklung geschuldet, da ich nicht wegem jeden neuen Display-Element das Gegenstück im Controller einpflegen will.
Aber Du hast recht. Ich werde mir eine elegantere Lösung überlegen.
- __blackjack__
- User
- Beiträge: 14052
- Registriert: Samstag 2. Juni 2018, 10:21
- Wohnort: 127.0.0.1
- Kontaktdaten:
Du schreibst im ersten Konzeptpunkt das die Werte in einem Dict angelegt werden, also nicht in einer Liste. Das wäre dann mit der Beschreibung der Semantik der Methoden und mit den Namen(sschreibweisen) aus dem ersten Beitrag mit der `nextion`-Klasse das hier:
Das `setElement()` nicht vorhandene Namen einfach ignoriert würde ich dann aber unbedingt auch dokumentieren, denn das ist ein bisschen überraschend das Verhalten.
Code: Alles auswählen
class nextion:
def __init__(self, uart):
...
self._elementDB = {}
...
def addElement(self, name, value):
if name in self._elementDB:
raise KeyError("{!r} already there".format(name))
self._elementDB[name] = value
def getElement(self, name):
return self._elementDB.get(name)
def setElement(self, name, value, refresh=False):
if name in self._elementDB:
self._elementDB[name] = value
if refresh:
self._sendVal(name, value)
...
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Das ist jetzt die aufgeräumte Nextion Klasse.
Ich war der Meinung das ich bereits ein Dict verwende was aber nicht stimmte. Aber wie das bei Software ja so ist, Projekt und Programmierer entwickeln sich während der Umsetzung immer weiter 
Herzlichen Dank an alle für Eure Inputs. Ich hatte ja schon mit einigen Programmiersprachen hantiert. Basic mit Zeilennummern , Basic ohne Zeilennummern, VisualBasic, Pascal, Assembler (vor allem Atmel), C (Arduino) und jetzt Python. Damals habe ich OOP kaum verstanden, jetzt mit Python ist das viel einfacher und logischer.
Code: Alles auswählen
class Nextion:
GET_PAGE_DATA = const(0x70) # Zeichen p
SET_NUM = const(0x4e) # Zeichen N / Store numerical value in Dictionary
GET_NUM = const(0x6e) # Zeichen n / Read numerical value from Dictionary
SET_TXT = const(0x54) # Zeichen T / Store string value in Dictionary
GET_TXT = const(0x74) # Zeichen t / Read string value from Dictionary
ACTION_SET = const(0x42) # Zeichen B / Set virtual Button / Action True
ACTION_RESET = const(0x62) # Zeichen b / Set virtual Button / Action False
COM_STATE_IDLE = const(0) # Waiting for COM_HEAD char
COM_STATE_READING = const(1) # Collecting Data
COM_STATE_EXECUTE = const(2) # Command end received, process command
COM_HEAD = const(0x40) # Zeichen @ / Head of command
COM_DELIMITER = const(0x24) # Zeichen $ / Delimiter used to separate name and value in command
COM_TAIL = const(0x23) # Zeichen # / End of command
def __init__(self, uart):
self._uart = uart
self._com_state = COM_STATE_IDLE
self._com_data = []
self._element_db = {}
self._sysvars = ["sys0", "sys1", "sys2"]
def _send(self, buf):
# send data to Nextion, add 'end of message' tail
self._uart.write(buf.encode())
self._uart.write(b'\xff\xff\xff')
def _c2t(self, data):
# convert bytearray to string
data_string = ''.join([chr(b) for b in data])
return data_string
def _c2n(self, data):
# convert bytearray to integer
data_string = ''.join([chr(b) for b in data])
return int(data_string)
def _send_val(self, name, value):
# prepare data string depending on type
if isinstance(value, str):
self._send(name + ".txt=" + '"' + value + '"')
else:
if name in self._sysvars:
self._send(name + "=" + str(value))
else:
self._send(name + ".val=" + str(value))
def add_element(self, name, value):
# add element to dictionary
# if element is already in dictionary, overwrite value
self._element_db[name] = value
def get_element(self, name):
# Caution! if element is not found, None ist returned
return self._element_db.get(name)
def set_element(self, name, value, refresh = False):
# ignore if name is not found in dictionary
if name in self._element_db:
self._element_db[name] = value
# if True send value to Nextion too
if refresh:
self._send_val(name, value)
def refresh_element(self, name):
# send element to Nextion if in dictionary
if name in self._element_db:
self._send_val(name, self.get_element(name))
def _execute_request(self, page, part):
# process incomming command
command = self._com_data[0]
if command == GET_PAGE_DATA:
page = chr(self._com_data[1])
part = "0"
if len(self._com_data) == 3:
part = chr(self._com_data[2])
elif command in [SET_TXT, SET_NUM]:
name, delimiter, value = (
self._com_data[1:].decode("latin-1").partition(COM_DELIMITER)
)
if not delimiter:
raise ValueError(
"delimiter {!r} not found in {!r}".format(
COM_DELIMITER, self._com_data
)
)
self.set_element(name, int(value) if command == SET_NUM else value)
print("SET_TXT/NUM:", name, value)
elif command in [GET_TXT, GET_NUM]:
name = self._c2t(self._com_data[1:])
self.refresh_element(name)
print("GET_TXT/NUM:", name)
elif command == ACTION_SET:
name = self._c2t(self._com_data[1:])
self.set_element(name, 1)
print("ACTION_SET:", name)
elif command == ACTION_RESET:
name = self._c2t(self._com_data[1:])
self.set_element(name, 0)
print("ACTION_RESET:", name)
else:
print("Nextion: unknown request:", self._com_data)
return page, part
def update(self):
# read serial incoming buffer for new commands
page = part = "0"
while self._uart.in_waiting:
byte_value = self._uart.read(1)[0]
if self._com_state == COM_STATE_IDLE:
if byte_value == COM_HEAD:
self._com_data.clear()
self._com_state = COM_STATE_READING
elif self._com_state == COM_STATE_READING:
if byte_value == COM_TAIL:
self._com_state = COM_STATE_EXECUTE
else:
self._com_data.append(byte_value)
# else:
# assert False, "unexpeted state {!r}".format(self._com_state)
if self._com_state == COM_STATE_EXECUTE:
self._com_state = COM_STATE_IDLE
if len(self._com_data) > 1:
page, part = self._execute_request(page, part)
return page, part

Herzlichen Dank an alle für Eure Inputs. Ich hatte ja schon mit einigen Programmiersprachen hantiert. Basic mit Zeilennummern , Basic ohne Zeilennummern, VisualBasic, Pascal, Assembler (vor allem Atmel), C (Arduino) und jetzt Python. Damals habe ich OOP kaum verstanden, jetzt mit Python ist das viel einfacher und logischer.