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)

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

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

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

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

{
  "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)

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

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

Downloads

Gerber

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

Software-Image

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

und natürlich der

Code

https://bitbucket.org/bobbery/regulatorcard (ist im Moment noch nicht öffentlich weil ich noch auf die finale Leiterplatte warte)

Gehäuse

Hier kommen 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

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 🙁

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

CodenamePCD ( Polarization-Check-Device )
Kunde:Führender Hersteller von Test und Messequipment
Projektzeitraum:etwa 6 Monate
Produktive Stunden:etwa 20
Tarif:Regulär
Einsatzzweck:Polarisationsprüfung von Batteriegebinden für den Rack-Einbau
Quellcode:Diesmal nix 🙂
Neu erworbene Skills:In EasyEDA mittlerweile sehr geübt – Neu dazu Generierung von Bestückungsdaten

Leiterplatten fertigen und gleich bestücken lassen ist echt bezahlbar
(JLCPCB ist der absolute Hammer !!!)
Rückblick:Es war ein supercooles Projekt. Ansprechpartner beim Kunden war mein Kumpel Ferdi. Wir hatten einen riesen Spaß und natürlich viele Biere miteinander 🙂
Kundenstimme:Fehlt noch
Continue reading

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

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

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