by Stefan·Kommentare deaktiviert für RegulatorCard
Regeln günstig und mit wenig Aufwand mit ESP32 und Micropython
Anforderungen
Billig – ein kompletter Aufbau mit Display etc. für unter 50€
Einfach zu programmieren – du brauchst nur eine Hand voll Python Kommandos kennen um professionelle Regelungen zu realisieren
Remote übers Netz programmierbar – die RegulatorCard verbindet sich über WLAN und bietet dir eine Webseite auf der du einfach dein Programm ändern kannst. Hast du am Standort der Regelung kein WLAN – so arbeitet die Karte als WLAN-AccessPoint
Schnittstellen für Visualisierungs/Einstellungsprogramme
Bei Bedarf erweiterbar über I2C (z.B. mehr Relais)
Einfach für Anfänger aber auch individuell nutzbar für embedded Profis die ihr komplett eigenes Programm schreiben wollen
Features
4 x PT-100 Analoge Temperatursensoren
3 x 18C20 Digitale Temperatursensoren
3 x Flusssensor
3 x PWM-Ausgang mit variabler Spannung
8 x Relais
1 x DotMatrix-Display via I2C
Hardware
Legende: V : Voltage/Versorgung; G : Ground; S: Signal; AP : AccessPoint; STA : Station; 5 : 5 Volt; VIn : Spannung für PWM
Relais im Betrieb / PWM für geregelte Pumpen (LED-Helligkeit als Demo)
Schaltplan
einfach und übersichtlich
Software
Das hier ist die Webseite mit der du deine Regelungen realisieren kannst. Es gibt links einen Bereich, in dem du die aktuellen Eingangs- und Ausgangswerte siehst. Rechts kannst du drauf los programmieren. Die Konstanten für dein Programm packst du am besten oben in die Settings (zeig später warum). Die Anmeldedaten sind initial admin/12345
Um das Programm zu ändern musst du:
Laufende Regelung erstmal mit dem Stop-Knopf stoppen.
Code ändern und ausführen (mit Execute Code) bis alles passt
Unter dem Programm ist eine grüne Box die dir anzeigt wie lange dein Programm gebraucht hat und auch deine print()-Ausgaben.
Ist der Rahmen grün ist alles Okay
Speichere mit Save Code deinen code in den controller
Jetzt kannst dann die Regelung mit dem Start-Knopf starten
Schaltest du aus und wieder an, so startet die RegulatorCard sofort wieder dein letztes Programm auszuführen
Schau dir den kurzen Beispielcode mal an:
# So Easy :-)
if SolarPumpMode == "AUTO" and t1 > SolarPumpOnValue:
pwm1 = 20 + min(80, (t1 - SolarPumpOnValue) * 3)
else:
pwm1 = 0
if flow1 > 100 and t5 > RegulatorValveOn:
relais1 = 1
else:
relais1 = 0
# Remember things over iterations
counter1 = get_var("counter1")
counter1 += 1
if counter1 > 5:
relais5 = 1
counter1 = 0
else:
relais5 = 0
set_var("counter1", counter1)
print("Dein erstes Programm läuft !!!")
Eigentlich ist in diesem Minimalbeispiel schon alles was du an Python brauchst um los zu programmieren
Ich hoffe du hast jetzt schon Lust bekommen damit zu Arbeiten 🙂
Aber einwas hab ich noch:
Schnittstellen
Ich hab vorher schon von Schnittstellen gesprochen.
Über den Endpoint http://<IP-Adresse>/data bekommst du die aktuellen Sensor und Aktorenwerte als einfaches JSON
Über den Endpoint http://<IP-Adresse>/save-settings kannst du diese in selber Form setzen (schau dir einfach den Code auf der Webseite dazu an)
Damit kannst du dir solche Visualisierungen bauen:
Visualisierung
Für die Visualisierung (und Einstellungen) reicht ein günstiges Android Tablet das kommt oft günstiger als RPI plus Display, Kabel Netzteil etc. Ein einfaches Tablet bekommt man bei Amazon schon ab etwas über 40€. Die Applikation ist diesmal Mit GoDot erstellt und lässt sicht leicht auf deine Wünsche anpassen
Ich werd wieder versuchen diesen Artikel als Dokumentation schrittweise zu verbessern
Aber schon mal viel Spaß damit Euer Stefan
Anhang
Software Architektur
einfach und robust
Einstellungen
Es gibt ein paar Einstellungen
PIN für die Webseite
WLAN SSID
WLAN Passwort
Führe einfach diesen Code aus um se zu setzen: (Vorher starten im AP-Modus ohne Jumper und verbinden mit dem WLAN RegulatorCard-XXX – IP-Adresse steht auf dem Display)
regulatorCardSettings("PIN","SSID","WIFI_PWD")
Den PIN brauchst du um auf die Webseite zu kommen. Initial sind die Anmeldedaten admin/12345 – danach admin/<deine PIN>
by Stefan·Kommentare deaktiviert für Signalverarbeitung mit (Micro)python
Ich möcht in diesem Beitrag zeigen, dass man Signalerarbeitung auch ohne viel Theorie und Mathematik hinbekommt und hab dafür eine Bibliothek geschrieben, die klein, supereinfach zu verwenden und vorallem auf einem ESP mit Micropython ihren Dienst tuen kann.
Signalverarbeitung war eines meiner Hauptfächer im Studium und ich kann mich noch an einen kompletten Leitzordner voller Theorie und Mathematik dazu erinnern. Sucht man im Internet nach z.B. einer FFT oder einen Filter in Python so findet man Programme die Bibliotheken verwenden, die es leider für Micropython nicht gibt und für die Ausführung auf einem Mikrocontroller nicht geeignet sind.
Eigentlich ist Signalverarbeitung recht einfach. Alles fing damit an, dass ein Französischer Mathematiker Jean Baptiste Joseph Fourier schon so um 1800 rum herausgefunden hat, dass sich jedes periodische Signal aus einer Überlagerung von Sinusschwingungen zusammensetzt.
Jede Sinusschwingung wird im Spektrum des Signals als Strich dargestellt. Hat man z.B. ein andersförmiges Signal, wie z.B. ein Rechteck, so besteht das Spektrum aus mehreren Strichen unterschiedlicher Frequenz und Amplitude.
Aber zurück zu unseren Sinusschwingungen. Ich hab ein kleines Programm geschrieben, das zwei Sinusschwingungen generiert und das zugehörige Spektrum anzeigt:
#####################################################################
# Testprogramm für b2c_sft : box2code - Signal Filter Toolbox
# Stefan am 2024-01-05
#####################################################################
######## Configure here:
sampleFreq = 2000
fftLen = 512 # must be power of 2
applyNotch = True
applyWinFilterBandStop = False
########################
import matplotlib.pyplot as plt
import b2c_sft
import math
data1 = b2c_sft.generateSinus(fftLen, 100, 250, sampleFreq)
data2 = b2c_sft.generateSinus(fftLen, 50, 500, sampleFreq)
data = b2c_sft.mixSignals(data1,data2)
if applyNotch:
notch = b2c_sft.Filter(b2c_sft.notchZeroes(500,sampleFreq), b2c_sft.notchPoles(500,sampleFreq))
data = notch.filterIterative(data)
zeroes1 = [
0.98811787286546693000, # comes from WinFilter
0.00000000000000263287,
2.96435361859640080000,
0.00000000000000526575,
2.96435361859640080000,
0.00000000000000263287,
0.98811787286546693000
]
poles1 = [
1.00000000000000000000, # comes from WinFilter
0.00000000013946281840,
2.98743365009436030000,
0.00000000021746471926,
2.97494613251504750000,
0.00000000001021009398,
0.98751223597926108000
]
if applyWinFilterBandStop:
bandStopFromWinFilter = b2c_sft.Filter(zeroes1, poles1)
data = bandStopFromWinFilter.filterIterative(data)
plt.subplot(2, 1, 1)
plt.plot(data)
plt.xlabel('Sample')
plt.ylabel('Amplitude')
b2c_sft.fft(data)
plt.subplot(2, 1, 2)
plt.suptitle('Zeit- und Frequenzsignal')
plt.plot(b2c_sft.frequencyAxis(data, sampleFreq), data)
plt.xlabel('Frequenz in Hz')
plt.ylabel('Amplitude')
plt.show()
Ausschauen tut das ganze dann so (hab Thonny verwendet mit dem Lokalen Python-Interpreter. Die mathplotlib musst du dir vielleicht mit pip erst installieren – Der Code für die Bibliothek b2c_sft kommt etwas weiter unten):
Bei Bedarf kann man zwei Filter aktivieren. Der eine ist ein NotchFilter, der gezielt eine Sinusschwingung aus dem Spektrum herausfiltert:
Man sieht dass die Schwingung bei 500 Hz jetzt weg ist
Du hast hier bestimmt schon ein paar Fragen:
Die Samplefrequenz ist doch 2000 – warum geht das Spektrum nur bis 1000
Zwei andere berühmte Mathematiker haben im Nyquist-Shannon-Abtasttheorem bestimmt, dass die Abtastfrequenz mindestens doppelt so hoch sein muss wie die höchste im Signal vorkommende Frequenz. D.h. der schnellste messbare Sinus darf höchstens halb so schnell sein wie die Samplefrequenz. Tatsächlich ist eine normale FFT um den Mittelpunkt spiegelsymmetrisch und enthält auch „negative Frequenzen“ aber weil ich diese Information in der Praxis für unnötig halte halbiere ich in der Lib die FFT nach der Berechnung. Dadurch gibt es keine Verwirrung.
Das ist doch eine Filterbibliothek – Es gibt nur einen Notch-Filter – wie mach ich andere Filter wie Tief- oder Hochpass ?
Der Filter in der Bibliothek nimmt Listen von Polen und Nullstellen als Parameter (Eigentlich die Koeffizienten eines Polynoms mit entsprechenden Nullstellen, aber ich hab ja versprochen keine Mathematik 🙂 ). Das genügt um jeden erdenklichen Filter damit zu bauen. Aber wie kommt man an die Werte für Pole und Nullstellen ?
Hinweis: beim FIR-Filter gibt es nur einen Array – das sind dann deine zeroes – Übergeb dann für die pole einfach eine leere Liste.
Kopiere dir das raus und packe es, Python konform, in dein Programm. Der Rest ist einfach:
zeroes1 = [
0.98811787286546693000, # comes from WinFilter
0.00000000000000263287,
2.96435361859640080000,
0.00000000000000526575,
2.96435361859640080000,
0.00000000000000263287,
0.98811787286546693000
]
poles1 = [
1.00000000000000000000, # comes from WinFilter
0.00000000013946281840,
2.98743365009436030000,
0.00000000021746471926,
2.97494613251504750000,
0.00000000001021009398,
0.98751223597926108000
]
bandStopFromWinFilter = b2c_sft.Filter(zeroes1, poles1)
data = bandStopFromWinFilter.filterIterative(data)
Du siehst, dass du den Sinus bei 500 Hz auch mit einer Bandsperre aus WinFilter ausblenden kannst (leider nicht so exakt wie mit dem Notch. Der Notch ist Teil der Bibliothek, weil WinFilter diesen nicht anbietet, ich ihn aber für sehr sehr nützlich halte)
Mit diesen paar kleinen Hilfsmitteln kannst du schon die meisten Filteraufgaben lösen.
Und auf den ESP damit 🙂
Hier einfach ein Cheap-Yellow-Display als Referenzplatform
Benchmark
Als erstes brauchen wir ein Gefühl dafür wie lange die Berechnung einer FFT dauert. Dazu ein kleines Testprogramm:
import b2c_sft
import utime
lens = [16, 32, 64, 128, 256, 512, 1024, 2048]
for fftLen in lens:
data = b2c_sft.generateSinus(fftLen, 100, 250, 2000)
start = utime.ticks_us()
b2c_sft.fft(data)
time = utime.ticks_us() - start
print("FFT-Länge " + str(fftLen) + " => " + str(time) + " us = " + str(time / 1000) + " ms")
""" Ausgabe:
FFT-Länge 16 => 3476 us = 3.476 ms
FFT-Länge 32 => 5576 us = 5.576 ms
FFT-Länge 64 => 10649 us = 10.649 ms
FFT-Länge 128 => 21342 us = 21.342 ms
FFT-Länge 256 => 48962 us = 48.962 ms
FFT-Länge 512 => 97913 us = 97.91301 ms
FFT-Länge 1024 => 219556 us = 219.556 ms
FFT-Länge 2048 => 494714 us = 494.714 ms
"""
Sehr schön zu sehen ist, dass wir bei jeder Verdoppelung der FFT-Größe etwa doppelt so lange brauchen. Eine 512er FFT in ca. 100 ms find ich schon ganz anständig – damit kann man was anfangen 🙂
ESP-Programm
Das erste ESP-Programm hat nur etwa 50 Zeilen und zeigt die FFT mit dem lvgl-Chart an. Ist eine 256er FFT mit 4kHz Abtastrate. Man sieht, dass das ziemlich flüssig geht.
#####################################################################
# Testprogramm für b2c_sft : box2code - Signal Filter Toolbox
# Stefan am 2024-01-05
#####################################################################
import machine, time
import uasyncio
import b2c_sft
import lvgl as lv
import display_driver
p0 = machine.Pin(21, machine.Pin.OUT)
p0 = 1
adc = machine.ADC(machine.Pin(35))
adc.atten(machine.ADC.ATTN_6DB)
adc.width(machine.ADC.WIDTH_9BIT)
tim = machine.Timer(1)
tsf = uasyncio.ThreadSafeFlag()
SAMPLE_FREQ = 4000
FFT_LEN = 256
data = []
timerStop = False
mid = 0
for i in range(0,1000):
mid += adc.read()
mid = mid / 1000
def newAdcVal(p):
global timerStop, tsf
if timerStop:
return
data.append(200 * (adc.read()-mid))
if len(data) == FFT_LEN:
timerStop = True
tsf.set()
chart = lv.chart(lv.scr_act())
chart.set_size(300, 220)
chart.align(lv.ALIGN.CENTER, 0, 0)
chart.set_range(lv.chart.AXIS.PRIMARY_Y, 0, 5000)
chart.set_style_size(0, lv.PART.INDICATOR)
ser = chart.add_series(lv.palette_main(lv.PALETTE.RED), lv.chart.AXIS.PRIMARY_Y)
chartData = []
for i in range(0, FFT_LEN // 2):
chartData.append(0)
pcnt = len(chartData)
chart.set_point_count(pcnt)
chart.set_ext_y_array(ser, chartData)
def run():
global timerStop
while True:
await tsf.wait()
b2c_sft.hammingWindow(data)
b2c_sft.fft(data)
for i in range(0, len(data)):
chartData[i] = int(data[i])
chart.set_ext_y_array(ser, chartData)
chart.refresh()
data.clear()
print("DONE")
timerStop = False
tim.init(freq=SAMPLE_FREQ, mode=machine.Timer.PERIODIC, callback=newAdcVal)
uasyncio.run(run())
So wie filtert man denn dann jetzt ?
Dazu gehört jetzt nicht mehr viel. Definiere dir im ESP-Programm deinen Wunschfilter (hier wieder ein Notch) – Die Werte vom ADC müssen jetzt nur noch durch den Filter „durch“ – geschafft 🙂
notch = b2c_sft.Filter(b2c_sft.notchZeroes(500,SAMPLE_FREQ), b2c_sft.notchPoles(500,SAMPLE_FREQ))
def newAdcVal(p):
global timerStop, tsf
if timerStop:
return
data.append(notch.filterReal(adc.read() - 256))
if len(data) == FFT_LEN:
timerStop = True
tsf.set()
Zum Ausprobieren einen Sinus von 0 bis 1000 Hz wobbeln – bei ca. 500 Hz sollte die Spitze verschwinden
Bibliothek
Die Bibliothek enthält natürlich wieder Bestandteile aus anderen Projekten:
Hauptteil der FFT aus SimpleFFT – eine schöne kleine C++ Bibliothek, die ich schon in ein paar C++ Projekten benutzt habe
by Stefan·Kommentare deaktiviert für BLE Sensoren mit Micropython (Daum Ergometer als Sensor)
Mein Vermieter Hans fragte mich neulich ob man irgendwie die Daten seines Ergometers (ein altes Daum ergo_bike) in seinen neuen Fahrradcomputer ( Einen Garmin Edge 820 ) bekommen kann, da er in seinem Fahrradcomputer gerne seine gesamten Fitnessdaten für Rennrad und Ergometer hätte
Bei Daum bekommt man für das ergo_bike eine schöne Schnittstellenbeschreibung:
Man kann erkennen, dass die sog. Run_Daten alles enthalten was man so braucht – Leistung, Trittfrequenz, Geschwindigkeit und Puls
Auf der Rückseite des Cockpits findet man den zugehörigen Stecker (oben):
Der Stecker sieht zwar wie RS-232 aus, hat aber etwas andere Pegel (Daum verwendet statt max232 2 Optokoppler für RX und TX) – Deswegen ein PullUp an pin 2 des MAX232.
GND und VCC hab ich der Übersichtlichkeit halber nicht eingezeichnet (und natürlich weil ich faul bin 🙂 )
Das war schon die Hardware – du wirst dich wundern warum 2 ESP32. Leider hat der Fahrradcomputer mehrere Sensoren pro Gerät nicht verstanden und es ging nicht anders 🙁
Nach etwas Micropython hats dann funktioniert:
Code Power-Sensor ( für Trittfrequenz, Leistung und Kadenz )
(oberer ESP32)
Tut mir leid, ist etwas schnell und schlampig geschrieben aber erfüllt seinen Zweck ganz gut
Das Protokoll zwischen Fahrradcomputern und Sensoren ist am besten beschrieben unter https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-5/ und natürlich viele viele andere Typen von Sensoren (z.B. Temperatur, Luftfeuchte …) man findet für fast Alles was. Vielleicht kannst du hier was brauchen für dein Sensorprojekt – es ist sehr leicht BLE-Sensoren zu implementieren ( besonders mit Micropython, finde ich 🙂 )
import bluetooth
import random
import struct
import time
from ble_advertising import advertising_payload
from micropython import const
from machine import UART
uart = UART(2, 9600)
uart.init(9600, bits=8, parity=None, stop=1)
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_INDICATE_DONE = const(20)
_FLAG_READ = const(0x0002)
_FLAG_NOTIFY = const(0x0010)
_FLAG_INDICATE = const(0x0020)
_UUID_POWER = bluetooth.UUID(0x1818)
_POWER_CHAR = (
bluetooth.UUID(0x2A63),
_FLAG_READ | _FLAG_NOTIFY | _FLAG_INDICATE,
)
_POWER_FEAT_CHAR = (
bluetooth.UUID(0x2A65),
_FLAG_READ | _FLAG_NOTIFY | _FLAG_INDICATE,
)
_POWER_LOC_CHAR = (
bluetooth.UUID(0x2A5D),
_FLAG_READ | _FLAG_NOTIFY,
)
_SERVICE_POWER = (
_UUID_POWER,
(_POWER_CHAR,_POWER_FEAT_CHAR,_POWER_LOC_CHAR,),
)
SERVICES = (_SERVICE_POWER,)
class BLEErgo:
def __init__(self, ble, name="HansErgoPower"):
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq)
((self._handlePower, self._handlePowerFeat,self._handlePowerLoc,),) = self._ble.gatts_register_services(SERVICES)
self._connections = set()
self._payload = advertising_payload(
name=name, services=[_UUID_POWER]
)
self._advertise()
self.lastCrankRev = 0
self.lastWheelRev = 0
self.lastCrankEvent = 0
self.lastWheelEvent = 0
dat = bytearray()
dat.append(0x0C) # speed and cadence
dat.append(0x00)
dat.append(0x00)
dat.append(0x00)
self._ble.gatts_write(self._handlePowerFeat, dat)
dat = bytearray()
dat.append(6) # right crank
self._ble.gatts_write(self._handlePowerLoc, dat)
def _irq(self, event, data):
if event == _IRQ_CENTRAL_CONNECT:
conn_handle, _, _ = data
self._connections.add(conn_handle)
elif event == _IRQ_CENTRAL_DISCONNECT:
conn_handle, _, _ = data
self._connections.remove(conn_handle)
self._advertise()
elif event == _IRQ_GATTS_INDICATE_DONE:
conn_handle, value_handle, status = data
def setPower(self, pwr, rpm, speed, notify=False, indicate=False):
self.lastWheelRev += 1
self.lastCrankRev += 1
self.lastCrankEvent += int(60 / rpm * 1024)
rnd = int(15500 / speed)
self.lastWheelEvent += rnd
print(rnd)
data = bytearray()
data.append(0x30) # wheel and crank rev present
data.append(0x00)
data+= ((int(pwr)).to_bytes(2, 'little'))
data+= ((int(self.lastWheelRev)).to_bytes(4, 'little'))
data+= ((int(self.lastWheelEvent)).to_bytes(2, 'little'))
data+= ((int(self.lastCrankRev)).to_bytes(2, 'little'))
data+= ((int(self.lastCrankEvent)).to_bytes(2, 'little'))
data.append(0x00)
data.append(0x00)
data.append(0x00)
data.append(0x00)
data.append(0x00)
self._ble.gatts_write(self._handlePower, data)
if notify or indicate:
for conn_handle in self._connections:
if notify:
self._ble.gatts_notify(conn_handle, self._handlePower)
if indicate:
self._ble.gatts_indicate(conn_handle, self._handlePower)
def _advertise(self, interval_us=100000):
self._ble.gap_advertise(interval_us, adv_data=self._payload)
def app():
ble = bluetooth.BLE()
ergo = BLEErgo(ble)
while True:
dat = bytearray()
dat.append(0x40)
dat.append(0x00)
uart.write(dat)
time.sleep_ms(10)
dat = bytearray()
while uart.any():
dat += uart.read()
if len(dat) > 14:
speed = dat[7]
cadence = dat[6]
power = dat[5] * 5
pulse = dat[14]
if cadence == 0:
cadence = 1
if speed == 0:
speed = 1
print ("Leistung: " + str(power))
print ("Geschwindigkeit: " + str(speed))
print ("Kadenz: " + str(cadence))
ergo.setPower(power, cadence, speed, notify=True, indicate=False)
else:
ergo.setPower(42, 42, 42, notify=True, indicate=False)
time.sleep_ms(1000)
if __name__ == "__main__":
app()
Code Speed-Sensor:
(unterer ESP32)
import bluetooth
import random
import struct
import time
from ble_advertising import advertising_payload
from micropython import const
from machine import UART
uart = UART(2, 9600)
uart.init(9600, bits=8, parity=None, stop=1)
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_INDICATE_DONE = const(20)
_FLAG_WRITE = const(0x0001)
_FLAG_READ = const(0x0002)
_FLAG_NOTIFY = const(0x0010)
_FLAG_INDICATE = const(0x0020)
_UUID_SPEED = bluetooth.UUID(0x1816)
_SPEED_CHAR = (
bluetooth.UUID(0x2A5B),
_FLAG_NOTIFY,
)
_SPEED_FEAT_CHAR = (
bluetooth.UUID(0x2A5C),
_FLAG_READ,
)
_SPEED_LOC_CHAR = (
bluetooth.UUID(0x2A5D),
_FLAG_READ,
)
_SPEED_CONTROL_CHAR = (
bluetooth.UUID(0x2A55),
_FLAG_WRITE | _FLAG_INDICATE,
)
_SPEED_CONTROL_CHAR = (
bluetooth.UUID(0x2A55),
_FLAG_WRITE | _FLAG_INDICATE,
)
_SERVICE_SPEED = (
_UUID_SPEED,
(_SPEED_CHAR,_SPEED_FEAT_CHAR,_SPEED_LOC_CHAR,_SPEED_CONTROL_CHAR,),
)
SERVICES = (_SERVICE_SPEED,)
class BLEErgo:
def __init__(self, ble, name="HansErgoSpeed"):
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq)
((self._handleSpeed,self._handleSpeedFeat,self._handleSpeedLoc,self._handleSpeedControl,),) = self._ble.gatts_register_services(SERVICES)
self._connections = set()
self._payload = advertising_payload(
name=name, services=[_UUID_SPEED]
)
self._advertise()
self.lastWheelEvent = 0
self.lastWheelRev = 0
dat = bytearray()
dat.append(0x01) # speed
dat.append(0x00)
self._ble.gatts_write(self._handleSpeedFeat, dat)
dat = bytearray()
dat.append(12) # rear wheel
self._ble.gatts_write(self._handleSpeedLoc, dat)
dat = bytearray()
dat.append(0x02)
dat.append(0x00)
self._ble.gatts_write(self._handleSpeedControl, dat)
def _irq(self, event, data):
if event == _IRQ_CENTRAL_CONNECT:
conn_handle, _, _ = data
self._connections.add(conn_handle)
elif event == _IRQ_CENTRAL_DISCONNECT:
conn_handle, _, _ = data
self._connections.remove(conn_handle)
self._advertise()
elif event == _IRQ_GATTS_INDICATE_DONE:
conn_handle, value_handle, status = data
def setSpeed(self, speed, notify=False, indicate=False):
self.lastWheelRev += 1
self.lastWheelEvent += int(15500 / speed / 2)
data = bytearray()
data.append(0x01) # wheel rev present
data+= ((int(self.lastWheelRev)).to_bytes(4, 'little'))
data+= ((int(self.lastWheelEvent)).to_bytes(2, 'little'))
self._ble.gatts_write(self._handleSpeed, data)
if notify or indicate:
for conn_handle in self._connections:
if notify:
self._ble.gatts_notify(conn_handle, self._handleSpeed)
if indicate:
self._ble.gatts_indicate(conn_handle, self._handleSpeed)
def _advertise(self, interval_us=100000):
self._ble.gap_advertise(interval_us, adv_data=self._payload)
def app():
ble = bluetooth.BLE()
ergo = BLEErgo(ble)
dat = bytearray()
while True:
while uart.any():
dat += uart.read()
if len(dat) > 14:
speed = dat[7]
cadence = dat[6]
power = dat[5] * 5
pulse = dat[14]
if cadence == 0:
cadence = 1
if speed == 0:
speed = 1
dat = bytearray()
print ("Geschwindigkeit: " + str(speed))
ergo.setSpeed(speed, notify=True, indicate=False)
time.sleep_ms(100)
else:
#ergo.setSpeed(42, notify=True, indicate=False)
time.sleep_ms(1000)
if __name__ == "__main__":
app()
by Stefan·Kommentare deaktiviert für ESP32-Grafik mit LVGL und Micropython
Geile Grafik, mit wenig Aufwand, für den ESP32
In letzter Zeit benutze ich für den ESP und ESP32 immer mehr Micropython (wie z.B. hier: CLM, Nobb-E, MessageInAbottle) weils einfach verdammt schnell geht.
Um mit dem ESP32 noch umwerfende Grafik zu machen gibt es lv_micropython. Leider musst du dir das für deinen Controller selbst bauen und kannst nich sofort loslegen. Ich werd in diesem Betrag am Ende ein fertiges Binary von lv_micropython anhängen, damit du ohne großen Aufwand in diese geniale Grafikbibliothek einsteigen kannst.
Eine Übersicht was LVGL alles kann findest du hier LVGL-Api
Für Micropython mit LVGL gibt es einen online Simulator, mit dem du ohne Hardware alles an Grafik schon mal ausprobieren kannst.
Ich nutze den online Simulator für das komplette UI-Design – erst dann pack ichs auf den Controller. Und wenns etwas größer wird einfach den Linux/SDL-Port Das ist einfach und unglaublich effizient.
Aber am Ende muss es doch auf den Controller und dafür müssen wir ein wenig was tun.
Als erstes muss der Micropython-Interpreter mit LVGL-Bindings auf deinen Controller. Hier gibt es zwei unterschiedliche Varianten.
Einfache Programme laufen auf beiden, aber preislich unterscheiden se sich fast ned – Daher fällt die Auswahl leicht 🙂 Im ZIP-File am Ende findest du trotzdem einen Ordner für beide Varianten.
Was wir jetzt brauchen ist noch ein Display. Ich hab dieses hier schon ne Weile in Verwendung: AZ-Touch Gleich mit Gehäuse für faule Leute wie mich 🙂
Ich benutze für Micropython auf dem ESP32 meistens Thonny Der dient als IDE und kann auch Dateien auf das Flash-Dateisystem des Controllers schieben.
Eine Datei, die wir dorthin schieben müssen ist display_driver.py mit folgendem Inhalt:
Diese kannst du bei Bedarf an andere Displays anpassen
Jetzt kannst du dir noch eines der Simulator-Beispiele von hier in den Mu-Editor kopieren und einfach (auf dem Gerät) ausführen. Bist du zufrieden, dann schiebst du deinen Quellcode als main.py auf den Controller und beim nächsten Reset ist deine Grafik da und tut Dinge 🙂
Ich hab das mal mit dem Meter-Beispiel von oben gemacht:
Man beachte den schnellen Start (ca. 1 Sekunde). Das mit einem embedded Linux zu übertreffen wird schwierig 🙂
Leider war das Touch-Screen etwas zickig und ich musste den Treiber dafür etwas anpassen (Brauchte eine SoftSPI weil die Pins so komisch konfiguriert waren) Mit der richtigen display_driver.py klappts aber damit auch:
Und natürlich ein kleines Beispiel, was man damit machen kann:
Sender (mit Grafik)
import network
import espnow
import lvgl as lv
import display_driver
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.disconnect()
en = espnow.ESPNow()
en.active(True)
peer = b'\xff\xff\xff\xff\xff\xff' # MAC broadcast-address
en.add_peer(peer)
color = "red"
count = 1
def send():
en.send(peer, color + " " + str(count))
def event_handler(e):
global color
code = e.get_code()
obj = e.get_target()
if code == lv.EVENT.VALUE_CHANGED:
option = " "*10
obj.get_selected_str(option, len(option))
color = option.strip()
send()
print("Color: " + color)
roller1 = lv.roller(lv.scr_act())
roller1.set_options("\n".join([
"red",
"green",
"blue"]),lv.roller.MODE.INFINITE)
roller1.set_width(300)
roller1.set_visible_row_count(3)
roller1.align(lv.ALIGN.CENTER,0,-50)
roller1.add_event_cb(event_handler, lv.EVENT.ALL, None)
def slider_event_cb(e):
global count
slider = e.get_target()
slider_label.set_text("{:d}%".format(slider.get_value()))
slider_label.align_to(slider, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
count = int(slider.get_value() / 6)
send()
# Create a slider in the center of the display
slider = lv.slider(lv.scr_act())
slider.align(lv.ALIGN.CENTER,0,50)
slider.add_event_cb(slider_event_cb, lv.EVENT.VALUE_CHANGED, None)
# Create a label below the slider
slider_label = lv.label(lv.scr_act())
slider_label.set_text("0%")
slider_label.align_to(slider, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
Empfänger (mit Neopixel-Ring)
import network
import espnow
import time
import machine, neopixel
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.disconnect()
e = espnow.ESPNow()
e.active(True)
peer = b'\xff\xff\xff\xff\xff\xff' # MAC broadcast-address
e.add_peer(peer)
np = neopixel.NeoPixel(machine.Pin(5), 16)
def disp(msg):
part = msg.split(" ")
color = (255, 0, 0)
if "green" in part[0]:
color = (0, 255, 0)
if "blue" in part[0]:
color = (0, 0, 255)
print(part[1])
cnt = int(part[1].strip("'"))
for i in range(0, 15):
if i < cnt:
np[i] = color
else:
np[i] = (0,0,0)
np.write()
disp("green 3")
def messageReceived(obj):
host, msg = e.recv(100)
print(host, msg)
disp(str(msg))
e.irq(messageReceived)
while True:
time.sleep_ms(100)
print(".")
Hinweis: wenn man, wie ich, zu faul ist eine MAC-Adresse einzugeben, kann man mit EspNow auch einfach an die Broadcast-Addresse FF:FF:FF:FF:FF:FF senden. Dann geht’s einfach an alle ESPs in Reichweite 🙂
Vereinfachtes Arbeiten, ohne Gerät, mit Thonny
Beim Erstellen von UIs stört noch das ständige Übertragen des Programms auf den ESP und das dortige Testen – das geht auf dem PC viel schneller und komfortabler.
Was mich noch gestört hat, ist dass die API-Doku im Netz leider nur die C-API beschreibt und ich immer etwas umdenken muss. Hab mal versucht aus einer JSON-Datei die beim Build entsteht etwas HTML zu generieren. Taugt zwar bei den Datentypen immer noch C auf – aber ist nach Objekten und Methoden aufgeteilt. Weiß ned obs so schon brauchbar ist. Möchte ich euch aber nicht vorenthalten:
Viel Spaß damit. Wenn ihr Verbesserungsideen habt – einfach her damit 🙂
Micropython mit LVGL für Windows
Nach dem AppImage für Linux hab ich mich noch mal hingesetzt und auch eine EXE für Windows generiert. Leider hat Thonny für Windows kein [Micropython (local)] als Einstellung aber damit kannst du deine Programme jetzt auch unter Windows laufen lassen. Was ich sehr cool finde ist dass der Interpreter nur ca. 5 MB groß ist.
Daniel hat mich vor einiger Zeit kontaktiert und mir erzählt, dass es das schöne günstige Board jetzt bei AliExpress für unter 7 € gibt. Diese Boards haben aber scheinbar Rot und Grün anders gedreht. Abhilfe schafft eine kleine Änderung in der display_driver.py:
Gestern hat mich Thomas über das Kontaktformular kontaktiert und mir erzählt, dass er versucht das Image mit SquareLine Studio zu verwenden. SquareLine Studio ist ein GUI-Designer, mit dem du deine GUI grafisch entwerfen kannst.
Wir haben rausgefunden, dass es geht wenn man ein paar Kleinigkeiten beachtet:
Beim Erzeugen des Projekts solltest du folgende Einstellungen verwenden:
Wichtig ist die LVGL-Version v8.3.6, da das Image auf v8.3.7 basiert
Die generierten Dateien deines Projekts schiebst du dann einfach mit Thonny auf deinen ESP.
Zusätzlich brauchst du noch folgenden einfachen Inhalt in deiner main.py, da der Treiber vom generierten Skript sonst nicht vor LVGL geladen wird
import display_driver
import ui # das ist die Datei mit deinem generierten code
SquareLine Studio ist ein sehr schönes Tool geworden und scheint gut zu funktionieren. Thomas hat gemeint, dass er eher aus der Hardwareecke kommt und das es genau das richtige Tool für ihn ist. Vielleicht gilt das ja auch für dich 🙂
Viele Screens mit wenig RAM-Verbrauch
Für viele scheint das wenige RAM z.B. bei Verwendung des CYD (Cheap Yellow Display) ein großes Problem zu sein. Ich hab heute mal versucht eine Blaupause zu erstellen, mit der man Programme mit unzähligen Screens machen kann ohne dass das RAM ausgeht. Der Trick dabei ist es, ausschließlich den angezeigten Screen ins RAM zu laden. Das geht eigentlich sehr sehr einfach. Das Programm unten simuliert 100 Screens und braucht nur wenige Kilobyte RAM (nämlich immer nur für den angezeigten Screen). Hoffe euch gefällts:
import lvgl as lv
import display_driver
import gc
class ScreenWithButtonOnly:
def __init__(self, text, nextScreenNum):
self.text = text
self.nextScreen = nextScreenNum
def load(self):
self.screen = lv.obj()
self.btn = lv.btn(self.screen)
self.btn.add_event_cb(self.eventhandler, lv.EVENT.ALL, None)
self.btn.align(lv.ALIGN.CENTER, 0, 0)
self.btn.set_size(200,100)
self.label = lv.label(self.btn)
self.label.set_text(self.text)
def eventhandler(self, e):
if e.code == lv.EVENT.CLICKED:
changeScreen(self.nextScreen)
class ScreenWithChartAndButton:
def __init__(self, text, nextScreenNum):
self.text = text
self.nextScreen = nextScreenNum
def load(self):
self.screen = lv.obj()
self.chart = lv.chart(self.screen)
self.chart.set_size(200, 150)
self.chart.align(lv.ALIGN.TOP_MID, 0, 10)
self.chart.set_type(lv.chart.TYPE.LINE)
self.ser1 = self.chart.add_series(lv.palette_main(lv.PALETTE.RED), lv.chart.AXIS.PRIMARY_Y)
self.ser1.y_points = [0, 20, 40, 60, 80, 100, 80, 60, 40, 20, 0]
self.chart.refresh()
self.btn = lv.btn(self.screen)
self.btn.add_event_cb(self.eventhandler, lv.EVENT.ALL, None)
self.btn.align(lv.ALIGN.BOTTOM_MID, 0, -10)
self.btn.set_size(300,50)
self.label = lv.label(self.btn)
self.label.set_text(self.text)
def eventhandler(self, e):
if e.code == lv.EVENT.CLICKED:
changeScreen(self.nextScreen)
class ScreenWithCalendarAndButton:
def __init__(self, text, nextScreenNum):
self.text = text
self.nextScreen = nextScreenNum
def load(self):
self.screen = lv.obj()
self.calendar = lv.calendar(self.screen)
self.calendar.set_size(200, 160)
self.calendar.align(lv.ALIGN.TOP_MID, 0, 10)
self.btn = lv.btn(self.screen)
self.btn.add_event_cb(self.eventhandler, lv.EVENT.ALL, None)
self.btn.align(lv.ALIGN.BOTTOM_MID, 0, -10)
self.btn.set_size(300,50)
self.label = lv.label(self.btn)
self.label.set_text(self.text)
def eventhandler(self, e):
if e.code == lv.EVENT.CLICKED:
changeScreen(self.nextScreen)
def changeScreen(screenNum):
screens[screenNum].load() # build the new screen
# load the new screen (animated) and delete the old one
lv.scr_load_anim(screens[screenNum].screen, lv.SCR_LOAD_ANIM.FADE_ON, 100, 0, True)
gc.collect() # call the garbage collector
print("MEM free: " + str(gc.mem_free())) # show free memory
screens = []
for i in range(0, 100):
if i % 2 == 0:
screens.append(ScreenWithChartAndButton("Screen " + str(i), i + 1))
elif i % 3 == 0:
screens.append(ScreenWithCalendarAndButton("Screen " + str(i), i + 1))
else:
screens.append(ScreenWithButtonOnly("Screen " + str(i), i + 1))
screens.append(ScreenWithButtonOnly("Back to begin", 0))
changeScreen(0) # load initial screen