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 oder zusätzliche Eingänge)
  • 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)

Gehäuse

Das Gehäuse besteht aus 3 3D-Druck Teilen und kann so mit jedem normalen 3D-Drucker gedruckt werden. STL-Dateien findest du weiter unten.

Du brauchst zum Druck (mit Support) etwa 180g Filament (mit einer Filamentrolle etwa 5 komplette Gehäuse) – bei 14€ für die Rolle sind das etwa 3€ fürs komplette Gehäuse

Das Gehäuse hat die Abmessungen 145x130x90 mm (LxBxH)
Die Befestigung ist für die Hutschiene vorgesehen

Du brauchst insgesamt 16 Schrauben

  • 4 x M3 fürs Display – nehm am Besten Kunststoffschrauben 14 mm reichen knapp aber nimm lieber 20 mm mit Muttern und schneide den Überstand ab
  • 6 x M3 Befestigung der Hutschienenclips am Unterteil 10 mm selbstschneidende Blechschrauben (es reichen eigentlich auch 4)
  • 4 x M3 für die Befestigung der beiden Unterteile an der Platine 10 mm selbstschneidende Blechschrauben
  • 1 x M3 Links und 1 x M3 Rechts zum fixieren der oberen Abdeckung 10 mm selbstschneidende Blechschrauben
  • kein Kleben – alle Komponenten sind durch das Zusammenstecken bereits fixiert und haben kaum Spiel

Display

Das Display zeigt den aktuellen Modus RUN / STOP / HANG

Du bekommst die Information ob AP AccessPoint oder STA Wlan Station (Client) Modus aktiv ist und die IP-Adresse über die du dich verbinden kannst

Der #-Balken zeigt die Ausführungen deines Programms (Nicht unbedingt nützlich – aber die Zeile war noch frei 🙂 )

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 das WLAN-Passwort ist initial 12345678

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 :-)

# some substitution helps you to understand your code later
tempCollector = t1
tempMixer = t5
pwmSolarPump = pwm1
relaisThermNeeded = relais1
flowAfterMixer = flow1


if SolarPumpMode == "AUTO" and tempCollector > SolarPumpOnValue:
   pwmSolarPump = 20 + min(80, (tempCollector - SolarPumpOnValue) * 3)
else:
   pwmSolarPump = 0

if flowAfterMixer > 100 and tempMixer > RegulatorValveOn:
   relaisThermNeeded = 1
else:
   relaisThermNeeded = 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)

# back substitution (only outputs needed)
pwm1 = pwmSolarPump
relais1 = relaisThermNeeded
 
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

{
  "t1": "18",
  "t2": "-9999",
  "t3": "-9999"
  "t4": "18",
  "t5": "-9999",
  "t6": "-9999",
  "t7": "-9999",
  "relais1": 0,
  "relais2": 0,
  "relais3": 1,
  "relais4": 0,
  "relais5": 0,
  "relais6": 0,
  "relais7": 1,
  "relais8": 0,
  "flow1": 0,
  "flow2": 0,
  "flow3": 0,
  "pwm1": 0,
  "pwm2": 0,
  "pwm3": 0,
  "mode": "STOP",
  "error": ""
}

Über den Endpoint http://<IP-Adresse>/get-settings bekommst du die aktuellen Konstanten diesmal gleich als Python-Code

SolarPumpMode = "AUS"
SolarPumpOnValue = 7.0
RegulatorValveOn = 42.0

Ü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)

Über den Endpoint http://<IP-Adresse>/set-time kannst du im AP-Modus die Zeit setzen (Das Visualisierungs-Programm unten macht das natürlich) beim STA-Modus passiert das automatisch über die Netzwerkzeit und du brauchst dich nicht darum kümmern

curl -X POST http://192.168.42.1/set-time
-H "Content-Type: application/json"
-d '{"year": 2026, "month": 11, "day": 5, "weekday": 3, "hour": 14, "minute": 30, "second": 0}'

Damit kannst du dir solche Visualisierungen bauen:

Visualisierung

Im Textfeld wo gerade „Everything is okay !“ steht kommen deine print() Ausgaben die du während deines Programms machst. So siehst du gleich was los ist

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

Wenn du Fragen hast schreib mich gerne an

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>

BOM

IDNameDesignatorFootprintQuantityManufacturer PartPriceLink
10.1uC1,C2,C3C06033
2HDR-M-2.54_1x10J1HDR-M-2.54_1X1010.071
3HDR-M-2.54_1x4J2HDR-M-2.54_1X410.057
4HDR-M-2.54_1x2J3HDR-M-2.54_1X210.03
5400R1R06031
64k7R2,R3,R10,R11,R12R06035
71kR4,R7,R8R06033
8330R5,R6,R9R06033
9ESP32-DEVKITCU1BULETM-SMD_ESP32-DEVKITC1ESP32-DevKitChttps://www.amazon.de/dp/B074RGW2VQ
10MAX31865AAP+U2SSOP-20_L7.2-W5.3-P0.65-LS7.8-BL1MAX31865AAP+8.95https://www.digikey.de/de/products/detail/analog-devices-inc-maxim-integrated/MAX31865AAP-T/5396623
11ADG804YRM-REELU3MSOP-10_L3.0-W3.0-P0.50-LS5.0-BL1ADG804YRM-REEL0.868https://www.digikey.de/de/products/detail/analog-devices-inc/ADG804YRMZ/997617
12PCF8574TS/3U4SSOP-20_L7.5-W5.2-P0.65-LS8.0-BL1PCF8574TS/3https://www.digikey.de/de/products/detail/texas-instruments/PCF8574PWR/484761
13JL301-50009U01U5,U6,U7,U8CONN-TH_JL301-50009U014JL301-50009U010.317https://www.amazon.de/dp/B0CBWXQQD1
14PC817X C档U9,U10,U11DIP-4_L6.5-W4.6-P2.54-LS7.6-BL3PC817X C档https://www.digikey.de/de/products/detail/sharp-socle-technology/PC817X2NSZ9F/7942022

Sensoren / Zubehör

Temperatursensor analoghttps://www.amazon.de/Temperaturf%C3%BChler-Temperatursensor-Wasserdichte-Silikon-Leitung-Speicherf%C3%BChler/dp/B09CH7BVBG
Temperatursensor digitalhttps://www.amazon.de/Aideepen-Temperatursensor-Wasserdicht-Temperaturf%C3%BChler-kompatibel/dp/B0DB5THKWQ
Flusssensorhttps://www.amazon.de/dp/B0FQC7BC45
Displayhttps://www.amazon.de/AYWHP-LCD-Bildschirm-Anzeigemodul-Bildschirm-Kompatibel-Raspberry/dp/B0F3JHJ732
Relais-Kartehttps://www.amazon.de/Elegoo-Relaismodul-Optokoppler-Arduino-Raspberry/dp/B01M61VVGV
Hutschienen Montageclipshttps://www.amazon.de/dp/B0D8F1F17V

Downloads

Gerber

Hier der Gerber-Daten-Link – damit kannst du dir Leiterplatten z.B. bel JLC-PCB bestellen

Natürlich hab ich ein paar vorrätig und schicke dir gerne eine zu – schreib mich einfach an

Software-Image

Hier kommt bald das Image mit der kompletten Software das du direkt mit esptool oder Thonny auf deinen Controller flashen kannst

Hier schon mal der Befehl: esptool –baud 460800 write-flash 0x1000 RegulatorCard_v0_1.bin

Mist hab bei der finalen Leiterplatte einen Fehler gemacht und GPIO12 verwendet der für die Umschaltung der Flash-Spannung verwendet wird.
Glücklicherweise gibt es einen Softwarepatch: führe einfach espefuse –port /dev/ttyUSB0 set-flash-voltage 3.3V aus (Das Programm ist Teil der esptools)

und natürlich der

Code

Firmware: https://bitbucket.org/bobbery/regulatorcard/commits/tag/v0.1

und die Visualisierung: https://bitbucket.org/bobbery/regulatorapp/src/main/

Gehäuse

Hier die STL-Daten für das Gehäuse

Danksagungen

Danke an Hans Markl für die erste Projektidee ( mal wieder 🙂 )

Danke an Hans Reif, meinem Elektronikervorbild, für viele tolle Gespräche mit Hopfengetränk und die Tipps beim Entwurf der Elektronik

Danke an meine Bürokollegen Michi, Leon und Jens mit eurer unkomplizierten Denkweise in Sachen Software und eure tollen Denkanstöße

Danke an Susanne dir kann ich immer schreiben was ich grad mach auch wenn ich grad Mist bau und du findest irgendwie Alles toll – ich bin so froh dass wir wieder Kontakt haben – hab dich sehr vermisst. Weiß blos immer ned was ich schreiben soll – hoffe dich störts ned dass es meistens Technik is

Wir haben das ganze mittlerweile etwas professioneller aufgesetzt – schau mal: RegulatorCard

Hans M. und ich ham vor über 8 Jahren mal eine Regelung für sein Ferienhaus gebaut: https://stefan.box2code.de/2018/05/11/warm-wasser-steuerung/
Neulich kamen wir mal wieder auf das Thema und ham entschieden dass wir das heute viel einfacher, besser und vorallem schneller könnten. Tatsächlich ham wir bis jetzt nur 2 Wochenenden dafür gebraucht

Die neue Regelung basiert jetzt auf Linux mit QT/QML-Applikation. Zu finden hier: https://bitbucket.org/bobbery/wwc2/src/main/

Alles in Allem besteht die ganze Anwendung aus unter 500 Zeilen einfachen QML-Code der unglaublich primitiv und daher wahrscheinlich auch gut zu warten ist.
Der C++ – Code enthält nur ein paar Treiber, mit denen man aus dem QML-Code heraus leicht die Hardware ansprechen kann.

Die Applikation ist noch nicht ganz fertig (es fehlt noch die eigentliche Regelschleife), da sich ein Großteil der Hardware grad noch in Italien befindet. Ich schreib hier wenn gar fertig und mit Hardware getestet

Update 2026-07-14: Regelung ist komplett implementiert und rudimentär getestet

Das Hauptbild ist einfach mit Inkscape und Cliparts von https://openclipart.org/ gezeichnet

Natürlich lassen sich die Parameter der Regelung leicht über TouchPad ändern und beiben nach Neustart erhalten:

Du kannst dir das Repo klonen und erstmal einfach im QtCreator auf deinem PC ausführen. Etwas interessanter ist aber wie man das Ding auf den Linux-Controller bekommt und wie man am geschicktesten die Hardware anbindet

Alles bissle fixiert für Transport nach Italien

Die Hardware

  • RPI-Zero 2W: weil der schön klein und billig is 🙂
  • HDMI-TS-Display 7″ – bekommt man für um die 40€ – über HDMI und USB angeschlossen
  • Einen MAX31865 – mit dem kann man einfache Widerstands-Temperatursensoren (PT-100) auslesen – wir brauchen 4 Sensoren aber nur einen MAX31865 da wir die Sensoren einfach über Relais multiplexen
  • Eine 8-Fach Relaiskarte – bekommt man für unter 10€
  • Einen PCF8574 – mit dem bekommen wir die Relaiskarte angesteuert
  • Einen Flusssensor, der impulse liefert wenn Wasser durchfließt

Alles in allem unter 100€ Material. Das Gehäuse ist einfach mit dem 3D-Drucker gedruckt.

Befestigt ist das Ganze mit etwas Bauschaum. Wenn du dich fragst warum Bauschaum: Wir hatten auf Arbeit mal bei einem Prototypen ein Display mit Heißkleber fixiert. Leider stand das ganze dann mal für eine Weile in der Hitze von Miami und viel auseinander 🙁 Deshalb auf keinen Fall mehr Heißkleber 🙂

Wir benutzen am RPI nur wenige Pins:

  • GPIO23 als Eingang für den Flusssensor (auf Tastaturtaste F12 gemapped)
  • SDA und SCL (GPIO2 & 3) zum Ansteuern der Relaiskarte über PCF8574 (da man mehrere Bausteine an einem Bus betreiben kann könnten wir damit sogar bis zu 64 Relais schalten)
  • MOSI, MISO, SCK, CS (GPIO8-11) zum Auslesen der PT-100 Sensoren über MAX31865

Normalerweise bau ich mir fast immer ein eigenes Linux-Image mit buildroot. Da hier aber Einfachheit zählt und die Bootzeit ziemlich egal ist, nehmen wir einfach ein fertiges Raspberry Pi OS Lite

Wie man ein eigenes Linux aufsetzt findest du hier: https://stefan.box2code.de/2022/05/27/embedded-linux-remote-debugging-mit-rpi-zero-buildroot-und-qt/

Linux Image vorbereiten

Am einfachsten geht das über den Raspberry Pi Imager. Sorge beim Flashen nur dafür, dass SSH aktiviert und mit deinem WLAN verbunden wird.

Läuft dein PI, dann verbinde dich über SSH und installiere mit sudo apt install qt6-*{dev}* cmake Alles was wir so brauchen.

Ändere deine /boot/firmware/config.txt auf folgenden Inhalt

dtparam=spi=on
dtparam=i2c_arm=on
dtoverlay=vc4-kms-v3d
dtoverlay=gpio-key,gpio=23,active_low=1,gpio_pull=up,keycode=88 # mapped to F12

Danach ist ein Reboot angebracht

Kopiere den Quellcode in dein Heimatverzeichnis auf dem PI (mit scp, FileZilla, WinSCP, o.ä.)
Mache dir ein Verzeichnis build in deinem Heimatverzeichnis auf dem PI und führe aus dem Verzeichnis heraus cmake ../wwc2; make aus.

Jetzt kannst du die Applikation mit ~/build/appwwc2 -platform eglfs starten

Damit das beim starten automatisch passiert, kannst du den Befehl an deine ~/.bashrc am Ende anhängen

Aktiviere noch schnell über sudo raspi-config den Auto-Login

Wenn du jetzt neu startest, sollte nach dem Booten die Applikation laufen

(optional) Image wartbar machen

Damit du auch außerhalb deines WLANs auf deinen PI zugreifen kannst, packst du in deine /etc/profile die Zeile nmcli device wifi hotspot ssid wwc2 password 12345678 so macht dein PI einen WLAN-Hotspot auf. SSH funktioniert dann über die Adresse 10.42.0.1

Und noch a einfaches Schaltbild

Noch nicht ganz fertig aber schon mal eine kleine Vorschau

Zielsetzung:

  • Einfach zu erlernen – die meisten konnten innerhalb von 15 Minuten loskonstruieren
  • Einfach erweiterbar – die Shape-Bibliothek besteht aus einfachen lua-Skripten die sich super mit KI-Hilfe generieren lassen. Werde euch ne kleine Webseite aufsetzen auf der ihr eure Erweiterungen teilen könnt
  • Interoperabel – manche Sachen (z.B. Kugellager) lassen sich leichter mit OpenSCAD konstruieren. Du kannst diese einfach importieren und in SketchIt3D weiterbearbeiten
  • Kostenlos und im Quellcode verfügbar (unter der LGPL)
  • Multi-Plattform dank QT ( fürs erste Linux und Windows )

Du bist mutig und willst ausprobieren ? – Aber Vorsicht ist noch experimentell !!!

https://files.box2code.de/
Benutzername: SketchIt3D
Passwort: test
Tut mir leid derzeit ist die Windows Version noch aktueller (hab zu wenige Linux Tester – hast du vielleicht Lust dazu ?)
Kurzes Video zu älterer Version is auch dabei

Eine brauchbare Software entsteht nicht ohne die Hilfe von netten Leuten die einem gute Ratschläge geben oder sogar richtig eintauchen und neue Versionen ausgiebig testen
Vielen Dank an Ferdi, Hans, Eva, Christian, Michi, Irek, Arvid, Tim, Klaus und Gerald

Eine Bitte hätte ich. Wir erleben auf Arbeit gerade eine große Flaute in Form von fehlenden Aufträgen.
Es wäre schön, wenn du jemanden kennst der einen guten Entwicklungsdienstleister sucht, an uns weiterleitest.
Wir sind https://www.corscience.com und sind Experten für MedTech – würden uns aber auch über Aufträge aus anderen Bereichen freuen

Die erweiterbare Shape-Bibliothek

Die Texturen die du siehst kannst du auch selbst erweitern. Leg einfach JPG-Dateien in den Ordner ~/SketchIt3D/OwnMaterials

Parameter für den ausgewählten Shape

Import aus OpenSCAD und das View-Menü

Objekte im Raum bewegen (geht auch über Tastatur-Shortcuts)

Objekte aneinander ausrichten

Lua-Code für Shapes inspizieren

Und ne kleine Hilfe

Gruppieren dauert manchmal etwas länger (es wird durch CGAL neue Geometrie berechnet)

Export für 3D-Drucker (hier im Anycubic Slicer)

Was fehlt noch ?

  • Hilfe fehlt noch komplett
  • Alignment Menü ist noch nicht vollständig und hat noch Fehler
  • Undo und Redo fehlt noch
  • Ich möchte noch einen Export für den Mehrfarbendruck einbauen

Update 2026-06-14: v0.1.3 alignment Menü funktioniert

Hans und ich ham uns mal wieder was überlegt.
Wir wollten uns ein Board bauen, mit dem wir unsere Standardhardware ohne Fädelaufbauten leicht ansteuern können

Jedes mal herauszusuchen, welche ESP32 Pins für was geeignet sind ist auf Dauer etwas nervig
und Fädelaufbauten sind sehr fehleranfällig.
Wir wollten daher einfach eine solide Plattform für Alles Mögliche.

Wir wollten:

  • Vernünftige JTAG-Schnittstelle für Debugging mit dem ESP-Prog aus VSCode heraus (mit PlatformIO)
  • I2C-Schnittstelle die direkt für MCP23017, PCF8574 (beides Portexpander) für DotMatrixDisplays und unsere RTC passt
  • SPI-Schnittstele die auf einen SD-Karten-Adapter oder ein TF-Display passt
  • Verstärkte Versorgung für NeoPixel, damit wir auch große Matrixen ansteuern können

Unsere (momentane) Standardhardware:

  • MCP23017 => https://www.amazon.de/Waveshare-MCP23017-IO-Expansion-Board/dp/B07P2H1NZG
  • DotMatrix-Display über I2C => https://www.amazon.de/AZDelivery-HD44780-Display-Zeichen-Schnittstelle/dp/B07DDGNPX1
  • RTC => https://www.amazon.de/Hailege-Uhr-Modul-Echtzeit-Uhr-Modul-RTC-Modul-Batterie/dp/B07XYZ7MCY
  • SD-Card-Adapter => https://www.amazon.de/AZDelivery-Reader-Speicher-Memory-Arduino/dp/B077MCQS9P
  • ESP-Prog => https://www.amazon.de/Peukerty-Esp-Prog-Downloader-Development-kompatibel-Schwarz/dp/B0BPRNKS8H
  • NeoPixel => https://www.amazon.de/GERUI-Steuerungen-Textanzeige-Einzeladressierbare-Vollfarbfunktionen/dp/B0DQSLL8P5

Passende Stecker findet man z.B. hier: https://www.amazon.de/QUARKZMAN-Weiblich-Breadboard-Flachbandkabel-mehrfarbig/dp/B0CTKJL1M1

Dafür ham wir uns ein kleines BreakOut-Board für die ESP32-NodeMCU (https://www.amazon.de/gp/product/B0D9BTQRYT)
entworfen (weil die schon Stecker hat und sich einfach auflöten lässt)

Unser Board:

Und unser Schaltplan:

Gerber-Dateien kommen, wenn wir uns sicher sind, das Alles funktioniert.

Teststatus:
SPI => OK
I2C => OK (Achtung DotMatrix-Display braucht 5V – ham wir ned wirklich berücksichtigt)
JTAG => OK
Probleme:
Layout für NodeMCU ist etwas zu schmal – man muss ganz schön quetschen dass man das Modul nei bekommt 🙁

Warum sagt ihr denn nix ? => Hier die Gerber Daten:

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 ?

Dazu möchte ich dir WinFilter empfehlen:

Wenn du auf „C-Code Generieren“ klickst kommt als Ergebnis folgendes:

    float ACoef[NCoef+1] = {
        0.98811787286546693000,
        0.00000000000000263287,
        2.96435361859640080000,
        0.00000000000000526575,
        2.96435361859640080000,
        0.00000000000000263287,
        0.98811787286546693000
    };

    float BCoef[NCoef+1] = {
        1.00000000000000000000,
        0.00000000013946281840,
        2.98743365009436030000,
        0.00000000021746471926,
        2.97494613251504750000,
        0.00000000001021009398,
        0.98751223597926108000
    };

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.

Hab ein Mikro an GPIO35 angeschlossen: https://www.amazon.de/dp/B08T1V3752 und etwas gepfiffen

#####################################################################
# 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
  • Notch-Berechnung von hier
  • Filter aus generiertem Code von WinFilter abgeleitet
#####################################################################
# b2c_sft : box2code.de - Signal Filter Toolbox
# Stefan am 2024-01-05
#####################################################################
import math

def isPowerOfTwo(num):
    return num and (not(num & (num - 1))) 

def rearrangeData(data):
    num_elements = len(data)
    buf = complex(0,0)
    target_index = 0
    bit_mask = 0
    for i in range(0, num_elements):
        if target_index > i:
            buf = data[target_index]
            data[target_index] = data[i]
            data[i]= buf
        bit_mask = num_elements
        while True:
            bit_mask = bit_mask >> 1
            if not (target_index & bit_mask):
                break
            target_index &= ~bit_mask
        target_index |= bit_mask

def makeTransform(data):
    counter = 0
    num_elements = len(data)
    local_pi = -math.pi
    nextbit = 0
    match = 0
    sine = 0.0
    delta = 0.0
    mult = complex(0,0)
    factor = complex(0,0)
    product = complex(0,0)

    i = 1
    while i < num_elements:
        nextbit = i << 1 
        delta = local_pi / i
        sine = math.sin(0.5 * delta)
        mult = complex(-2.0 * sine * sine, math.sin(delta))
        factor = complex(1.0)   

        for j in range(0,i):
            k = j
            while k < num_elements:
                counter += 1
                match = k + i
                product = data[match] * factor
                data[match] = data[k] - product
                data[k] += product
                k += nextbit
            factor = mult * factor + factor;
        i = i << 1
    return True
        
def backToReal(data):
    num_elements = len(data)
    for i in range(0, num_elements): 
        data[i] = abs(data[i]) / num_elements  
        
def fft(data):
    if not isPowerOfTwo(len(data)):
        print ("Not power of 2 !!!")
        return False
    rearrangeData(data)
    if not makeTransform(data):
        return False
    backToReal(data)
    n = len(data)
    for i in range(0, n // 2):
        data.pop()
    return True

def frequencyAxis(data, fs):
    num_elements = len(data)
    axis = []
    for i in range(0, num_elements):
        axis.append(fs / num_elements / 2 * i)
    return axis
    

def generateSinus(num_elements, amplitude, f, fs):
    sinus = []
    ta = 1 / fs
    t = 1/f
    for i in range(0, num_elements):
        sinus.append(amplitude * math.sin(i * 2*math.pi*ta/t))
    return sinus
    
def mixSignals(sig1, sig2):
    num_elements = min(len(sig1),len(sig2))
    data = []
    for i in range(0, num_elements):
        data.append(sig1[i] + sig2[i])
    return data


def notchPoles(f, fs):
    poles = []
    return poles
    
def notchZeroes(f, fs):
    zeroes = [0,0,0]
    zeroes[0] = 1.0
    zeroes[1] = -2.0 * math.cos(2 * math.pi * f / fs)
    zeroes[2] = 1.0
    return zeroes

def iirNotchZeroes(f, fs):
    b0=1.0
    b1=-2.0*math.cos(2.0*math.pi*f/fs);
    b2=1.0;
    return [b0,b1,b2]
    
def iirNotchPoles(f, fs):
    r=0.9;
    a0=1.0
    a1=-2.0*r*math.cos(2.0*math.pi*f/fs)
    a2=r*r
    return [a0,a1,a2]

class Filter:
    def __init__( self, zeroes, poles ):
        self.zeroes = zeroes
        self.poles = poles
        self.filterlen = len(zeroes)
        self.x = []
        self.y = []
        for i in range(0, self.filterlen + 1):
            self.y.append(0.0)
            self.x.append(0.0)  

    def filterReal(self, newSample): 
        # shift old samples
        for i in range(self.filterlen, 0, -1):
            self.x[i] = self.x[i-1]
            self.y[i] = self.y[i-1]
        # calc new output
        self.x[0] = newSample
        self.y[0] = self.zeroes[0] * self.x[0];
        for n in range(1, self.filterlen):
            self.y[0] += self.zeroes[n] * self.x[n]
            if len(self.poles) > 0:
                self.y[0] -= self.poles[n] * self.y[n]
        return self.y[0]
    
    def filterIterative(self, data):
        output = []
        for newSample in data:
            output.append(self.filterReal(newSample))    
        return output

def hammingWindow(data):
    for n in range(0, len(data)):
        data[n] = data[n] * (0.54 - 0.46 * math.cos((2 * math.pi / len(data)) * n))




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

Du brauchst Micropython und die Datei https://github.com/micropython/micropython/blob/master/examples/bluetooth/ble_advertising.py aus den Micropython-Beispielen auf deinem Device. Natürlich hab ich mich auch großzügig an den Beispielen dort bedient – ist doch klar 🙂

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()

Ich hab mir vor einiger Zeit ein HANMATEK HO52S Taschenoszi gekauft und bin total begeistert von dem Gerät. Analogbandbreite von 50 MHz, 2 Kanäle und einen integrierten Funktionsgenerator – und das ganze für unter 200 €

Das einzige von dem ich nicht begeistert war, war die PC-Software dazu. Leider nur Windows und unter VirtualBox gings leider auch ned 🙁 Hab mir die Anleitung angeschaut und gesehen, dass man damit eh nicht sehr viel machen kann außer Daten vom Oszi runterzuladen. Eine wirkliche Bedienung des DSOs wäre damit garnicht möglich gewesen.

Hab mir das Gerät etwas genauer angeschaut und wollte rausfinden ob es irgendeine Möglichkeit gibt es auch über den PC anzusprechen. Nach etwas Google hab ich schnell herausgefunden, dass die Firmware des Geräts eigentlich von Owon kommt. Für einige Geräte von Owon gibt es eine Schnittstellendoku im Netz.

Zu meiner großen Freude benutzt das Gerät das supereinfache SCPI-Protokoll über USB – Und ich hatte schon ein Ziel für meinen Urlaub 🙂

Das Bild indem die Kurven dargestellt werden, hab ich einfach vom Oszi selber – Screenshot gemacht, 3x vergrößert und alle Werte wegradiert 🙂 Dann einfach die Werte von der Schnittstelle drüberzeichnen.

Die Software müsste auch für andere ähnliche DSOs von Owon und OEMs funktionieren. Ich würde mich freuen von dir zu hören, wenn du ein anderes Gerät erfolgreich damit zum Laufen bekommst.

Der Quellcode (natürlich GPL) liegt unter https://bitbucket.org/bobbery/qpocketscope (ist sehr einfach nur ca. 300 LOC)

Als erstes kommen natürlich die Linux-Nutzer dran:

https://stefan.box2code.de/huge_files/QPocketScope_2024_11_05.tar.gz

Im Archiv findest du ein AppImage, dass du einfach ausführen kannst. Leider musst du noch (als root) install.sh ausführen. Das fügt eine udev-Rule hinzu ohne die eine kommunikation mit dem DSO nicht möglich ist (zumindest als nicht-root).

Die Windows-Benutzer müssen sich noch etwas gedulden. Ich brauche jemand mit einem Windows-Rechner zum Testen, da es leider unter VirtualBox nicht geht (die SW vom Hersteller aber auch nicht).
Natürlich würde ich mich riesig freuen, wenn ein/e Windows-Nutzer/in mit DSO dies liest und sich zur Verfügung stellt 🙂

Viel Spaß damit – euer Stefan

StateMachines sind das Brot und Butter eines (embedded) Softwareentwicklers. Die meisten StateMachine Frameworks für C++ sind aber sehr kompliziert und verwenden unglaublich viel Templates oder hängen von irgendwelchen Bibliotheken ab. Hab grad Urlaub und hab mir gedacht ich bau mir jetzt endlich ein eigenes Statemachine-Framework. Überraschenderweise war das recht schnell erledigt (ca. 2 Tage). So viel muss ein StateMachine-Framework ja eigentlich garned können hab ich gemerkt. Was mich sehr gewundert hat ist, dass sowas wie parallele States in vielen Frameworks garnicht möglich sind oder sehr aufwendig. Rausgekommen sind im ersten Schuss etwa 100 Zeilen Code in einer einzigen Headerdatei. Auch wenn ich beim Testen recht leicht auf 100% Coverage kam, bin ich nicht sicher, dass das Ding fehlerfrei ist. Also erstmal vorsichtig damit – Wenn du Fehler findest, bitte her damit. Ich hab bestimmt ein paar versteckt 🙂

Features

  • Eventbasiert
  • Einfache Initialisierung (alles an einer Stelle bzw. Methode)
  • SubStates und parallelle States
  • Single header mit nur etwa 130 LOC – reinkopieren und loslegen – keine Abhängigkeiten zu irgendwelchen Bibliotheken – STL only
  • Interface mit nur einer Methode die lediglich Events entgegennimmt
  • Fast keine Templates – C++11 – auch für alte Compiler
  • Geeignet für Embedded-Systeme (auch Bare-Metal mit wenig RAM, FreeRTOS, Arduino, mbed)
  • Verschachtelbar – mehrere StateMachines in einem System einfach möglich
  • Wenig Speicherbedarf – eine einfache StateMachine braucht ca. 1,4 kB (gemessen auf ESP32 mit Arduino)
  • Schnell – Stateübergang in 5 us auf PC. Auf ESP32 in 75 us (ohne Logging)
  • Initialisierung aller States, Transitions und Reactions in nur einer überschriebenen Methode
  • Einfach zu verbinden mit dem Rest des Systems – kleine Aktionen über Lambdas die mit dem „Rest“ verknüpfen
  • So einfach, dass durch das kleine Beispiel bereits getestet mit 100% LineCoverage (coverage als html liegt mit im Repo)
  • Update 2024-11-04: Hab einen Timer-Mechanismus eingebaut (ist zwar kein standard StateMachine-Feature, hab ich aber schon oft gebraucht)

Das Repo liegt hier: https://bitbucket.org/bobbery/mystatemachine hab qmake verwendet, damit mans leicht im QtCreator aufmachen und übersetzen kann. Braucht natürlich kein Qt – aber für ein Makefile für jede Plattform bin ich zu faul und qmake hab ich viel lieber als CMake

Das ist die StateMachine – ist einfach nur ein Header mit knapp über 100 LOC

/* Copyright 2024 stefan.box2code.de
 *            
 * Use of this source code is governed by an MIT-style
 * license that can be found in the LICENSE file or at
 * https://opensource.org/licenses/MIT.
 */
#ifndef STATEMACHINE_H
#define STATEMACHINE_H

#include <functional>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>

#define LOG_EVENTS
#define LOG_STATES

template<class StateID, class EventID>
class StateMachine
{
public:
    StateMachine() {}

    void react(EventID e) { react_p(e); }
    void handleTimerTick() { handleTimerTick_p(); }

    virtual void init() = 0; // overwrite it to create your machine

    virtual ~StateMachine() {}

protected:
    void react_p(EventID e)
    {
#ifdef LOG_EVENTS
        if (m_eventNames.find(e) != m_eventNames.end()) {
            std::cout << "on" << m_eventNames.find(e)->second << "()" << std::endl;
        }
#endif
        bool transitOk = true;
        for (StateID sid : m_activeStates) {
            if (m_reactions.find(std::make_pair(sid, e)) != m_reactions.end()) {
                if (!m_reactions.find(std::make_pair(sid, e))->second()) {
                    transitOk = false;
                }
            }
        }
        if (transitOk) {
            transit(e);
        }
    }

    void addInitialState(StateID sid, const std::string &name, StateID parent = StateID::NumStates)
    {
        m_stateNames[sid] = name;
        if (parent != StateID::NumStates) {
            m_childStates[parent].insert(sid);
        } else {
            m_activeStates.insert(sid);
        }
        m_initials[parent].insert(sid);
    }
    void addState(StateID sid, const std::string &name, StateID parent = StateID::NumStates)
    {
        m_stateNames[sid] = name;
        if (parent != StateID::NumStates) {
            m_childStates[parent].insert(sid);
        }
    }
    void addEventName(EventID eid, const std::string &name) { m_eventNames[eid] = name; }
    void addReaction(StateID sid, EventID eid, std::function<bool()> fun)
    {
        m_reactions.emplace(std::make_pair(sid, eid), fun);
    }
    void addEntry(StateID sid, std::function<bool()> fun) { m_entry.emplace(sid, fun); }
    void addExit(StateID sid, std::function<bool()> fun) { m_exit.emplace(sid, fun); }
    void addTransition(StateID sid, EventID eid, StateID target)
    {
        m_transitions.emplace(std::make_pair(sid, eid), target);
    }

    void transit(EventID e)
    {
        std::set<StateID> newActiveStates;
        for (StateID id : m_activeStates) {
            if (m_transitions.find(std::make_pair(id, e)) != m_transitions.end()) {
                StateID activated = m_transitions.find(std::make_pair(id, e))->second;
                newActiveStates.insert(activated);
                if (m_initials.find(activated) != m_initials.end()) {
                    for (StateID subInitial : m_initials.find(activated)->second) {
                        newActiveStates.insert(subInitial);
                    }
                }
            } else {
                newActiveStates.insert(id);
            }
        }

        std::set<StateID> deactivated;
        for (StateID id : m_activeStates) {
            if (newActiveStates.count(id) == 0) {
                deactivated.insert(id);
            }
        }

        for (StateID id : deactivated) {
            if (m_childStates.find(id) != m_childStates.end()) {
                for (StateID chId : m_childStates.at(id)) {
                    newActiveStates.erase(chId);
                    if (m_exit.find(chId) != m_exit.end()) {
                        m_exit.at(chId)();
                    }
                }
            }
            if (m_exit.find(id) != m_exit.end()) {
#ifdef LOG_STATES
                std::cout << "onExit() of " << m_stateNames.at(id) << std::endl;
#endif
                m_exit.at(id)();
            }
        }

        for (StateID id : newActiveStates) {
            if (m_activeStates.count(id) == 0) {
                if (m_entry.find(id) != m_entry.end()) {
#ifdef LOG_STATES
                    std::cout << "onEntry() of " << m_stateNames.at(id) << std::endl;
#endif
                    m_entry.at(id)();
                }
            }
        }

        m_activeStates = newActiveStates;
#ifdef LOG_STATES
        std::cout << "Current States: ";
        for (StateID id : m_activeStates) {
            if (m_stateNames.find(id) != m_stateNames.end()) {
                std::cout << m_stateNames.find(id)->second << " ";
            }
        }
        std::cout << std::endl;
#endif
    }

    void handleTimerTick_p()
    {
        bool timerExpired = false;
        for (auto timer : m_timers) {
            if (timer.second < 0) {
                continue;
            }
            m_timers[timer.first] = --timer.second;
            if (timer.second == 0) {        // timer is expired
                m_timers[timer.first] = -1; // timer is stopped
                timerExpired = true;
            }
        }
        if (timerExpired) {
            react_p(EventID::TimerExpired);
        }
    }

    bool isTimerExpired(size_t timerNum)
    {
        if (m_timers.find(timerNum) == m_timers.end()) {
            return false;
        }
        return m_timers.at(timerNum) < 0;
    }

    void startTimer(size_t timerNum, int timeInMs) { m_timers[timerNum] = timeInMs; }

    void stopTimer(size_t timerNum) { m_timers[timerNum] = -1; }

    std::map<StateID, std::string> m_stateNames;
    std::map<EventID, std::string> m_eventNames;
    std::map<StateID, std::set<StateID>> m_initials;
    std::map<std::pair<StateID, EventID>, StateID> m_transitions;
    std::set<StateID> m_activeStates;
    std::map<StateID, std::set<StateID>> m_childStates;
    typedef std::function<bool()> reactFun;
    std::map<std::pair<StateID, EventID>, reactFun> m_reactions;
    std::map<StateID, reactFun> m_entry;
    std::map<StateID, reactFun> m_exit;
    std::map<size_t, int> m_timers;
};

#endif // STATEMACHINE_H

Und ein Besispiel

Das Beispiel ist bewusst einfach und hoffentlich leicht verständlich. Eigentlich wollt ich nur was was mit dem man gut parallelle States demonstrieren kann. Das Diagramm ist mit dem QtCreator gezeichnet:

/* Copyright 2024 stefan.box2code.de
 *            
 * Use of this source code is governed by an MIT-style
 * license that can be found in the LICENSE file or at
 * https://opensource.org/licenses/MIT.
 */

#include "statemachine.h"

#define MEASURE_TIME

#ifdef MEASURE_TIME
#include <chrono>
#endif

enum class MyStateID {
    Startup,
    Running,
    Lampe1An,
    Lampe1Aus,
    Lampe2An,
    Lampe2Aus,
    NumStates
}; // you should always add NumStates on end
enum class MyEventID {
    TimerExpired, // must always be there
    On,
    Off,
    Toggle,
    NumEvents
}; // you should always add NumEvents on end

class MyApi // The Interface to the Rest of your Application
{
public:
    virtual bool checkBatteryFull()
    {
        std::cout << "Implement battery check" << std::endl;
        return false;
    }
    virtual void turnOnLed() { std::cout << "Implement turn on LED" << std::endl; }
    virtual void playAudio() { std::cout << "Implement playing audio" << std::endl; }
    virtual void sendEventToAnotherSm(MyEventID)
    {
        std::cout << "Send event to another state machine in your system" << std::endl;
        // anotherStateMachine.react(e);
    }
    virtual ~MyApi() {}
};

class MyStateMachine : public virtual StateMachine<MyStateID, MyEventID>
{
public:
    MyStateMachine(MyApi &api)
        : m_api(api)
    {
        init();
    }

    void init() override
    {
        addInitialState(MyStateID::Startup, "Startup");
        addReaction(MyStateID::Startup, MyEventID::Off, [&]() {
            std::cout << "Hey a Off-Event in Startup" << std::endl;
            if (m_api.checkBatteryFull()) { // this is the easy way to call the rest of your system
                return true;                // must return true to do corresponding transition
            }
            return false;
        });
        addTransition(MyStateID::Startup,
                      MyEventID::Off,
                      MyStateID::Running); // this one will be forbidden by reaction above
        addTransition(MyStateID::Startup, MyEventID::On, MyStateID::Running);

        addState(MyStateID::Running, "Running");
        addEntry(MyStateID::Running, [&]() {
            std::cout << "Entry() of Running" << std::endl;
            startTimer(42, 500); // start timer 42 with 500 ms
            m_api.turnOnLed();   // this is the easy way to call the rest of your system
            return true;         // return what ever you want - doesnt matter
        });
        addExit(MyStateID::Running, [&]() {
            std::cout << "Exit() of Running" << std::endl;
            stopTimer(42); // timer 42 is now stopped
            return false;  // return what ever you want - doesnt matter
        });
        addTransition(MyStateID::Running, MyEventID::Off, MyStateID::Startup);
        addTransition(MyStateID::Running, MyEventID::TimerExpired, MyStateID::Startup);
        addReaction(MyStateID::Running, MyEventID::TimerExpired, [&]() {
            if (isTimerExpired(42)) {
                std::cout << "Timer 42 has expired" << std::endl;
                return true;
            }
            return false;
        });

        addInitialState(MyStateID::Lampe1An, "Lampe1An", MyStateID::Running);
        addTransition(MyStateID::Lampe1An, MyEventID::Toggle, MyStateID::Lampe1Aus);

        addState(MyStateID::Lampe1Aus, "Lampe1Aus", MyStateID::Running);
        addTransition(MyStateID::Lampe1Aus, MyEventID::Toggle, MyStateID::Lampe1An);
        addEntry(MyStateID::Lampe1Aus, []() {
            std::cout << "Entry() of Lampe1Aus" << std::endl;
            return true; // return what ever you want - doesnt matter
        });
        addExit(MyStateID::Lampe1Aus, []() {
            std::cout << "Exit() of Lampe1Aus" << std::endl;
            return false; // return what ever you want - doesnt matter
        });

        addState(MyStateID::Lampe2An, "Lampe2An", MyStateID::Running);
        addTransition(MyStateID::Lampe2An, MyEventID::Toggle, MyStateID::Lampe2Aus);
        addTransition(MyStateID::Lampe2An, MyEventID::Off, MyStateID::Startup);
        addReaction(MyStateID::Lampe2An, MyEventID::Off, []() {
            std::cout << "Hey a Off-Event in Lampe2An" << std::endl;
            return false; // returns false so the transition is skipped
        });

        addInitialState(MyStateID::Lampe2Aus, "Lampe2Aus", MyStateID::Running);
        addTransition(MyStateID::Lampe2Aus, MyEventID::Toggle, MyStateID::Lampe2An);

        addEventName(MyEventID::On, "On");
        addEventName(MyEventID::Off, "Off");
        addEventName(MyEventID::Toggle, "Toggle");
    }

private:
    MyApi &m_api;
};

int main()
{
    MyApi myApi;
    MyStateMachine m(myApi);
#ifdef MEASURE_TIME
    auto start = std::chrono::steady_clock::now();
#endif
    m.react(MyEventID::Off);
    m.react(MyEventID::On);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Off);
    m.react(MyEventID::Off);
    m.react(MyEventID::On);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Off);
    m.react(MyEventID::On);
    m.react(MyEventID::Off);
    m.react(MyEventID::On);

#ifdef MEASURE_TIME
    std::cout << "Elapsed(us)="
              << std::chrono::duration_cast<std::chrono::microseconds>(
                     std::chrono::steady_clock::now() - start)
                     .count()
              << std::endl;
#endif

    for (int ms = 0; ms < 1000; ms++) {
        // of course you should call this in your SysTick-Handler
        m.handleTimerTick();
    }
}

Du siehst schon – Templates brauchst du nur in einer Zeile um deine eigenen Listen für States und Events einbinden zu können.

Die Anbindung an dein restliches System hab ich nur skizziert.

Ich hoffe der Code ist ansonsten selbsterklärend – Viel Spaß damit

Ach ja, noch die Ausgabe des Programms:


onOff()
Hey a Off-Event in Startup
Implement battery check
onOn()
onEntry() of Running
Entry() of Running
Implement turn on LED
Current States: Running Lampe1An Lampe2Aus 
onToggle()
onEntry() of Lampe1Aus
Entry() of Lampe1Aus
Current States: Running Lampe1Aus Lampe2An 
onToggle()
onExit() of Lampe1Aus
Exit() of Lampe1Aus
Current States: Running Lampe1An Lampe2Aus 
onToggle()
onEntry() of Lampe1Aus
Entry() of Lampe1Aus
Current States: Running Lampe1Aus Lampe2An 
onToggle()
onExit() of Lampe1Aus
Exit() of Lampe1Aus
Current States: Running Lampe1An Lampe2Aus 
onOff()
Exit() of Lampe1Aus
onExit() of Running
Exit() of Running
Current States: Startup 
onOff()
Hey a Off-Event in Startup
Implement battery check
onOn()
onEntry() of Running
Entry() of Running
Implement turn on LED
Current States: Running Lampe1An Lampe2Aus 
onToggle()
onEntry() of Lampe1Aus
Entry() of Lampe1Aus
Current States: Running Lampe1Aus Lampe2An 
onToggle()
onExit() of Lampe1Aus
Exit() of Lampe1Aus
Current States: Running Lampe1An Lampe2Aus 
onToggle()
onEntry() of Lampe1Aus
Entry() of Lampe1Aus
Current States: Running Lampe1Aus Lampe2An 
onToggle()
onExit() of Lampe1Aus
Exit() of Lampe1Aus
Current States: Running Lampe1An Lampe2Aus 
onOff()
Exit() of Lampe1Aus
onExit() of Running
Exit() of Running
Current States: Startup 
onOn()
onEntry() of Running
Entry() of Running
Implement turn on LED
Current States: Running Lampe1An Lampe2Aus 
onOff()
Exit() of Lampe1Aus
onExit() of Running
Exit() of Running
Current States: Startup 
onOn()
onEntry() of Running
Entry() of Running
Implement turn on LED
Current States: Running Lampe1An Lampe2Aus 
Elapsed(us)=123
Timer 42 has expired
Exit() of Lampe1Aus
onExit() of Running
Exit() of Running
Current States: Startup 

CodenameCFB ( CORmunity – Fotobox )
KurznamecorCAM
Kunde:Unsere CORmunity bei Corscience
Projektzeitraum:etwa 1,5 Monate
Produktive Stunden:etwa 60
Tarif:CORmunity-4-CORmunity ( nur Material )
Einsatzzweck:Partyfotos 🙂
Quellcode:Applikation etwa 1000 LOC
Neu erworbene Skills:Für Raspi immer das offizielle Netzteil verwenden ( sonst undervoltage )
Bügelbrett eignet sich hervorragend als Montageplattform 🙂
Fotografieren und Beleuchtung ( natürlich immer noch totaler Laie dadrin 🙂 )
Rückblick:Absoluter Funfaktor – so ein geiles Projekt 🙂
Danke an Sina für die super Idee und den Projektanstoß
Danke an Viktoria für die hervorragende Projektleitung und deine rat- und tatkräftige Unterstützung
Danke an Jörg für die Finanzierung und megaschnelle Bestellung unserer benötigten Komponenten
Danke an Sebastian für die tollen Tipps zum Fotografieren
Kundenstimme:fehlt noch

Motivation:

Bei unseren vielen Firmenevents mieten wir uns meist eine Fotobox, was uns oft mehrere hundert Euro kostet. Sina hatte die Idee, dass wir uns das sparen könnten wenn wir eine eigene Fotobox hätten.
Tatsächlich kamen wir mit dem Material für unsere eigene Fotobox, inklusive Stativ, Greenscreen etc. nur auf etwa 500 €

Erster Entwurf:

Aussen:

Eine Holzkiste gab es bei Amazon – mussten nur noch paar Löcher rein.
Unser leuchtendes Logo ist aus durchsichtigem Filament mit dem 3D-Drucker gedruckt.
Dahinter verbergen sich ein paar Nebelscheinwerfer 🙂

Innen:

Den Innenraum beherrscht eine Canon EOS 50d Spiegelreflexkamera, die wir gebraucht mit Objektiv für etwa 90€ bekommen ham. Außerdem noch ein Raspberry, paar Netzteile und Relaiskarte

Mit Stativ:

Das Stativ ist eigentlich ein Stativ für Scheinwerfer und wird einfach in einen Flansch auf der unteren Gehäuseseite geschraubt.

Video:

Bedienen kann man das Ganze entweder über eine Garagentüröffner-Fernbedienung oder halt den Touchscreen

Beispiel Greenscreen:

Als Hintergrund ham wir uns noch einen Greenscreen mit Stativen besorgt. Damit können wir etz nach Belieben den Hintergrund austauschen

Bilder runterladen:

Man kann sich mit der Box über WLAN verbinden und über FTP die Bilder runterladen

CodenameRDP ( Radarpistole )
Kunde:Hans Reif
Projektzeitraum:über Weihnachten
Produktive Stunden:etwa 20
Tarif:Friends (beer and food 🙂 )
Einsatzzweck:Geschwindigkeitsmessung mit günstigem Doppler-Radarsensor
Quellcode:ESP32-FW ca. 100 LOC
Neu erworbene Skills:Funktionsweise Doppler-Radar
Schaltungssimulation mit EasyEDA und Qucs-S
Rückblick:Wir sind noch nicht fertig und bauen jetzt das Gerät weiter zu einem autonomen System um, das an der Straße positioniert, Fahrzeuge auf beiden Fahrspuren erfasst und protokolliert
Kundenstimme:Später, wenn wir ganz fertig sind 🙂
Continue reading

CodenameTKA ( Ansteuerung für Temperaturkammer )
Kunde:Führender Hersteller von Test und Messequipment
Projektzeitraum:etwa 3 Tage
Produktive Stunden:etwa 3 – 4
Tarif:Wegen Minimalaufwand geschenkt
Einsatzzweck:Ansteuerung einer Temperaturkammer, mit Mustern definiert in Excel-Dokmument, via TCP-Socket-Protokoll
Quellcode:PC-SW ca. 250 LOC
Neu erworbene Skills:–
Rückblick:War sehr leicht umzusetzten, Protokoll der Kammer war gut beschrieben, Kunde war schon mit der ersten Demo zufrieden 🙂
Kundenstimme:Fehlt noch
Continue reading

CodenamePMGWeb (Webinterface für Pegelmessgerät)
Kunde:Hans Reif
Projektzeitraum:etwa 2 Monate
Produktive Stunden:etwa 20
Tarif:Friends ( beer and food 🙂 )
Einsatzzweck:Pegelstände von Flüssigkeiten darstellen und Messung triggern
Quellcode:Device ESP 300 LOC
WEB-Page 300 LOC
Neu erworbene Skills:Kann jetzt besser und schneller Webanwendungen mit UI5 bauen
ApexCharts funktionieren recht gut zusammen mit UI5
Rückblick:Es war ein sehr schönes Projekt, Hans war in meiner Lehrzeit mein Elektronikervorbild und es versteht sich von selbst dass wir viel zu quatschen und viel Spaß hatten 🙂
Kundenstimme:Es hat richtig Spaß gemacht, dieses Projekt mit Stefan zu machen. Es funktioniert super, genauso wie ich es mir vorgestellt habe. Stefan hat eine sehr professionelle Arbeit abgeliefert. Vielen Dank. Freue mich schon auf das nächste Projekt.
Continue reading

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.

Meine anderen Artikel zu Micropython und ESP:

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.

Ein cooles Beispiel ist z.B. das hier

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:

import ili9XXX
from xpt2046 import xpt2046
import lvgl as lv

disp = ili9XXX.ili9341(miso=19, mosi=23, clk=18, dc=4, cs=5, rst=22, power=-1, backlight=15, mhz=20, width=320, height=240, rot=ili9XXX.LANDSCAPE, factor=16)
touch = xpt2046(cs=14, transpose=False, cal_x0 = 3783, cal_y0 = 3600, cal_x1 = 242, cal_y1 = 123)

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 🙂

Downloads:

lv_micropython-WROOM.zip

lv_micropython-WROVER.zip
(hier hab ich auch noch die deutschen Umlaute mit in die Systemfont gepackt)

Neues Board

Bei Amazon gibt es ein schönes neues Board: https://www.amazon.de/dp/B0CSYPG716

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:

import sys
import espidf as esp
from machine import *
import ili9XXX
import lvgl as lv

disp = ili9XXX.ili9341(factor=32, double_buffer=False, clk=14, cs=15, dc=2, rst=12, power=23, backlight=21, miso=12, mosi=13,width=320, height=240, backlight_on=1, rot=ili9XXX.LANDSCAPE)
spi2=SoftSPI(baudrate=2500000,sck=Pin(25),mosi=Pin(32),miso=Pin(39))

class xpt2046:
    CMD_X_READ  = const(0x90)
    CMD_Y_READ  = const(0xd0)
    CMD_Z1_READ = const(0xb8)
    CMD_Z2_READ = const(0xc8)

    MAX_RAW_COORD = const((1<<12) - 1)

    def __init__(self, spi, cs, max_cmds=16, cal_x0 = 3783, cal_y0 = 3948, cal_x1 = 242, cal_y1 = 423,
                 transpose = True, samples = 3):

        # Initializations

        if not lv.is_initialized():
            lv.init()

        disp = lv.disp_t.__cast__(None)
        self.screen_width = 320
        self.screen_height = 240
        self.spi = spi
        self.cs = cs
        self.recv = bytearray(3)
        self.xmit = bytearray(3)

        self.max_cmds = max_cmds
        self.cal_x0 = cal_x0
        self.cal_y0 = cal_y0
        self.cal_x1 = cal_x1
        self.cal_y1 = cal_y1
        self.transpose = transpose
        self.samples = samples

        self.touch_count = 0
        self.touch_cycles = 0

        indev_drv = lv.indev_drv_t()
        indev_drv.init()
        indev_drv.type = lv.INDEV_TYPE.POINTER
        indev_drv.read_cb = self.read
        indev_drv.register()

    def calibrate(self, x0, y0, x1, y1):
        self.cal_x0 = x0
        self.cal_y0 = y0
        self.cal_x1 = x1
        self.cal_y1 = y1

    def deinit(self):
        print('Deinitializing XPT2046...')

    def touch_talk(self, cmd, bits):
        if self.cs is not None:
            self.cs(0)
        self.xmit[0] = cmd
        self.spi.write_readinto(self.xmit, self.recv)
        if self.cs is not None:
            self.cs(1)
        return (self.recv[1] * 256 + self.recv[2]) >> (15 - bits)

    # @micropython.viper
    def xpt_cmds(self, cmds):
        result = []
        for cmd in cmds:
            value = self.touch_talk(cmd, 12)
            if value == int(self.MAX_RAW_COORD):
                value = 0
            result.append(value)
        return tuple(result)

    # @micropython.viper
    def get_med_coords(self, count : int):
        mid = count//2
        values = []
        for i in range(0, count):
            values.append(self.xpt_cmds([self.CMD_X_READ, self.CMD_Y_READ]))
        x_values = sorted([x for x,y in values])
        y_values = sorted([y for x,y in values])
        if int(x_values[0]) == 0 or int(y_values[0]) == 0 : return None
        #print(x_values[mid], y_values[mid])
        return x_values[mid], y_values[mid]

    # @micropython.viper
    def get_coords(self):
        med_coords = self.get_med_coords(int(self.samples))
        if not med_coords: return None
        if self.transpose:
            raw_y, raw_x = med_coords
        else:
            raw_x, raw_y = med_coords

        if int(raw_x) != 0 and int(raw_y) != 0:
            x = ((int(raw_x) - int(self.cal_x0)) * int(self.screen_width)) // (int(self.cal_x1) - int(self.cal_x0))
            y = ((int(raw_y) - int(self.cal_y0)) * int(self.screen_height)) // (int(self.cal_y1) - int(self.cal_y0))
            return x,y
        else: return None

    # @micropython.native
    def get_pressure(self, factor : int) -> int:
        z1, z2, x = self.xpt_cmds([self.CMD_Z1_READ, self.CMD_Z2_READ, self.CMD_X_READ])
        if int(z1) == 0: return -1
        return ( (int(x)*factor) / 4096)*( int(z2)/int(z1) - 1)

    start_time_ptr = esp.C_Pointer()
    end_time_ptr = esp.C_Pointer()
    cycles_in_ms = esp.esp_clk_cpu_freq() // 1000

    # @micropython.native
    def read(self, indev_drv, data) -> int:

        esp.get_ccount(self.start_time_ptr)
        coords = self.get_coords()
        #print(coords)
        esp.get_ccount(self.end_time_ptr)

        if self.end_time_ptr.int_val > self.start_time_ptr.int_val:
            self.touch_cycles +=  self.end_time_ptr.int_val - self.start_time_ptr.int_val
            self.touch_count += 1

        if coords:
            data.point.x ,data.point.y = coords
            data.state = lv.INDEV_STATE.PRESSED
            return False
        data.state = lv.INDEV_STATE.RELEASED
        return False

    def stat(self):
        return self.touch_cycles / (self.touch_count * self.cycles_in_ms)

touch = xpt2046(spi=spi2, cs=Pin(33), cal_x1 = 3700, cal_y1 = 3820, cal_x0 = 180, cal_y0 = 250, transpose=False)
lv.init()

ESP-NOW

Ich hab mich riesig gefreut, dass Harald mich letzte Woche über das Kontaktformular angeschrieben und mir von EspNow erzählt hat. EspNow ist ein sehr einfaches Protokoll, mit dem sich ESPs untereinander kurze Nachrichten senden können ohne sich vorher zu verbinden (verwenden dazu einfach die WiFi-MAC-Addresse). Unter https://docs.espressif.com/projects/esp-faq/en/latest/application-solution/esp-now.html findet man erste Informationen zu diesem coolen Protokoll. Und natürlich in der Micropython-Doku: https://docs.micropython.org/en/latest/library/espnow.html

Ich kannte EspNow garnicht, finds aber superklasse und hab Harald versprochen, dass ichs natürlich einbaue, damit man auch grafisch rumfunken kann 🙂

So erstmal die Firmware, jetzt mit ESP-NOW:

Harald hat gemeint er benutzt gerne asyncio und braucht noch aioespnow:

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.

Alles was du brauchst ist das AppImage hier:

Den Rest kann Thonny:

Viel Spaß damit 🙂

API-Doku etwas mehr Python-Like

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:

https://stefan.box2code.de/static/Lvgl8.3_API_ForMicropython.html

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.


Hier der Download:

Cheap Yellow Displays mit gedrehten Farben

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:

disp = ili9XXX.ili9341(..., colormode=ili9XXX.COLOR_MODE_RGB)

Ich muss mir dort unbedingt auch ein paar holen 🙂

Image mit SquareLine Studio nutzen

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





CodenameCLM (Curved Light Modulator)
Kunde:Jörg vom Deep Zone Experience Project
Projektzeitraum:etwa 5 Monate
Produktive Stunden:etwa 60
Tarif:CORmunity ( pay in beer 🙂 )
Einsatzzweck:verraten wir nicht 🙂
Quellcode:Device 200 LOC
PC-Design-SW 550 LOC
WEB-Page 260 LOC
Android-App 200 LOC
Neu erworbene Skills:Einfacher und stromsparender Aufbau von batteriebetriebenen Geräten

Eigene Leiterplatten sind schnell entworfen und machen auch für Prototypen Sinn da unglaublich günstig – außerdem macht’s Spaß

Miniaturisierung und Optimierung für den Low-Cost Markt (Materialkosten < 7€ bei Einzelstückzahlen)

Lichtleiter mit 3D-Drucker ( danke Norbert 🙂 )
Rückblick:Es war ein sehr schönes Projekt, Jörg ist immer gut gelaunt, immer erreichbar, hat sehr gute Ideen, einen Sinn fürs Wesentliche und einen sehr netten Humor
Kundenstimme:Stefan hat die Projektidee schnell und einfach umgesetzt sowie im Projekt eine Menge guter Ideen eingebracht. Das Ergebnis lässt sich sehen und wir sind auch mit überschaubaren Ausgaben zum Ziel gekommen. Ich freue mich auf die nächste Ausbaustufe. 
Continue reading

Letztes Jahr hatte ich die Gelegenheit
etwas mit dem coolen HTML5-Framework eines Kunden zu machen.

Hab dann nach einer freien Alternative gesucht und bin bei
UI5 von SAP gelandet und bin voll begeistert.

Continue reading

svg2qml war kein guter Name – gab’s schon 🙂 Wir nennens jetzt mal InkBridge4Qml

Einfaches Tool zum generieren von QML aus SVG-Bildern. Funktionalität ist ähnlich zur kostenpflichtigen Qt Bridge for Adobe Photoshop.

Happy animating 🙂

Continue reading

Mal wieder ein größeres Projekt mit meinem Vermieter und Kumpel Hans zusammen. Ein Android-Fahrradcomputer mit offline Karte, BLE-Sensoren, Aufzeichnung und GPX-Export. Wieder mit meinem neuen Lieblingswerkzeug Dart & Flutter

Natürlich bei F-Droid

svg

Timo hat neulich den, total schicken, neuen Desktop Cutefish auf YouTube entdeckt.
Wir sind von der Schnelligkeit, Optik und Bedienung total begeistert.
Außerdem ist Cutefish in QT programmiert und benutzt einige Teile von KDE
(QT war immer mein Argument warum ich KDE und nichts anderes verwende 🙂 ).
Leider gibt es nur Downloads für ArchLinux und Manjaro.
Drum hier die Pakete für die letzte LTS von Ubuntu:

Continue reading

Schon die dritte Version des einfachen Getränkeautomats, die ich baue – Selbes Prinzip aber diesmal völlig neue Bauweise. Diesmal hab ich als Basis einfach ein sehr günstiges Android-Tablet verwendet. Für die Implementierung natürlich mein neues Lieblingswerkzeug Dart&Flutter.

Eine „ungebrandede“ Version des Mat-O-Id gibt es bei F-Droid

svg

Verwendete Komponenten:

Haehne 7″ Tablet (ca. 42€)
Achtung Preis schwankt von Tag zu Tag
USB-OTG-Adapter ist dabei und das Tablet hat einen alternativen Stecker zum Laden – ideal 🙂

RFID-Reader (11€)
Achtung es gibt Reader mit zwei unterschiedlichen Frequenzen – sollte zu den verwendeten Tokens passen.

Hab’s heute auf Arbeit gezeigt. Sina – unsere Expertin für Optik – hatte noch einige Verbesserungswünsche und hat mir ein wunderschönes Cormunity Logo gegeben. Sieht jetzt so aus:

Mal wieder ein richtiges Unsinnsprojekt 🙂
Ein Nudelkocher mit App- bzw. PC-Steuerung über Bluetooth
Noch nicht ganz fertig – aber mal die ersten Bilder

Continue reading

Ein Gerät zum Bezahlen von Getränken, mit einem RFID-Token, auf meiner Arbeit. Mittlerweile die zweite Version mit schönerer GUI durch TouchGFX. Zum alten Mat-O-Mat, der schon seit etwa zwei Jahren im Einsatz ist, schreibe ich was wenn dieser erfolgreich durch die neue Version abgelöst ist.

Continue reading

Projekt mit meinem Vermieter Hans zusammen – Eine Warm-Wasser Steuerung für ein Ferienhaus.
Das Projekt verwendet mein neues Lieblingsboard, das man zur Zeit für einen sagenhaften Preis
bei Amazon bekommt.
Hab mittlerweile schon 7 davon gekauft, eines kaputt gemacht und 4 davon verbaut.
Die ersten Gehversuche damit waren der Mat-O-Mat und zwei Analysegeräte für eine Pumpe. Zum Mat-O-Mat kommt bald was.
Verwendet hab ich dazu mbed und die ETL – eine spitzen Kombination, finde ich.

Continue reading

Vor kurzem bin ich von AVR Mikrocontrollern auf ARM umgestiegen.
Nach ein wenig Herumprobieren fehlt jetzt ein anständiges Projekt.
Ich hab lange überlegt und jetzt endlich was Passendes gefunden.
Dann mal los …

Continue reading

Lange Zeit hab ich mich bei Mikrocontrollern auf 8-Bit beschränkt (8051 und AVR).
Ich dachte immer die 32-Bit ARM Controller wären zu teuer und für die meisten Anwendungen sowieso überdimensioniert.
Aber das stimmt bei weitem nicht mehr…

Continue reading

Mal eine kleine Fingerübung für zwischendurch.
Auf Arbeit müssen wir oft zwischen verschiedenen Koordinatensystemen konvertieren.
Abhilfe soll der kleine Rechner hier schaffen, der hoffentlich noch um viele Funktionen erweitert wird.

Continue reading

Nicht wirklich ein Freizeitprojekt, da ich den größten Teil auf Arbeit geschrieben habe
wegen Lizenzanforderungen von PCLint und Visual Studio

Continue reading

Gemeinsames Projekt mit Bernd. Ziel ist ein Retro Spielautomat für alte Arcade Klassiker mit dem RaspberryPI.
Bernd ist gerade dabei ein Gehäuse zu bauen und ich hab mich derweil um ein einfaches Menü und
eine Möglichkeit zum Anschluss eines klassischen Joysticks an den PI gekümmert

Continue reading

Nach Langem endlich mal wieder ein Projekt in Qt das richtig Spaß macht 🙂 Spaß macht es vor allem durch die vielen Ideen, Verbesserungswünsche und Bugreports von Kollegen, die das Tool verwenden. An dieser Stelle noch mal vielen Dank dafür. Ich hoffe es gibt noch viel zu tun …

Continue reading

Einige wissen vielleicht wie das ist – man findet eine coole Bibliothek und will damit unbedingt was tolles machen. Glücklicherweise hatte ein Kollege ein passendes Problem, das sich gut für ein erstes Projekt eignete …

Continue reading

Wer mit mir schon gearbeitet hat weiß, dass ich ein Fan von Anekdoten bin. Oft sagt nämlich ein kurzer Satz mehr aus als das was in einem ganzen Buch niedergeschrieben ist …

Continue reading

Zauberkünstler ist ein kleines Programm, dass ich vor einigen Jahren mit C++ und QT, für einen mir bekannten Zauberer geschrieben habe. Es war damals (ca. 2005) eines meiner ersten größeren Programme, die ich mit C++ geschrieben habe. Daher hat es vielleicht nicht gerade das beste objektorientierte Design, ist aber sehr einfach aufgebaut und gut zu verstehen.

Continue reading