Im Angebot von AZ-Delivery sind die folgenden TFT-Farbdisplays mit SPI Interface.
Dieses Display nutzt den Treiberbaustein ST7735. Dieser hat einen Bildspeicher mit 132 x 162 x 18 Bits. Mit der GFX-Bibliothek werden für dieses Display davon nur 128 x 160 x 16 Bits genutzt.
Link zum Produkt
Pins auf der Display Seite von links nach rechts:
GND VCC SCK SDA RES RS CS LED
Die Betriebsspannung an VCC und an den Eingängen darf maximal 3.3 V betragen. Bei einem 5V Mikrocontroller sind Pegelwandler erforderlich.
Die Spannung für die Hintergrundbeleuchtung am Pin LED muss 3.3V betragen.
Dieses Display nutzt auch den Treiberbaustein ST7735. Dieser hat einen Bildspeicher mit 132 x 162 x 18 Bits. Mit der GFX-Bibliothek werden für dieses Display davon nur 128 x 160 x 16 Bits genutzt.
Link zum Produkt
Pins auf der Display Seite von links nach rechts:
VCC GND CS RES RS SDA SCK LED
Die Betriebsspannung an VCC und an den Eingängen darf maximal 5 V betragen funktioniert aber auch mit 3.3 V.
Die Spannung für die Hintergrundbeleuchtung am Pin LED muss 3.3V betragen.
Dieses Display nutzt den Treiberbaustein ST7789V2. Dieser hat einen Bildspeicher mit 240 x 280 x 18 Bits. Mit der GFX-Bibliothek werden für dieses Display davon nur 240 x 280 x 16 Bits genutzt.
Link zum Produkt
Pins auf der Display Seite von links nach rechts:
LED CS RS RES SDA SCL VCC GND
Die Betriebsspannung an VCC und an den Eingängen sollte 3.3V betragen.
Die Hintergrundbeleuchtung ist standardmäßig eingeschaltet und kann mit LOW am Pin LED ausgeschaltet werden.
Dieses Display nutzt den Treiberbaustein GC9A01A. Dieser hat einen Bildspeicher mit 320 x 240 x 18 Bits. Mit der GFX-Bibliothek werden für dieses Display davon nur 240 x 240 x 16 Bits genutzt.
Link zum Produkt
Pins auf der Display Seite von links nach rechts:
RES CS RS SDA SCK GND VCC
Die Betriebsspannung an VCC und an den Eingängen sollte 3.3V betragen.
Hintergrundbeleuchtung ist immer eingeschaltet.
Dieses Display nutzt den Treiberbaustein ILI9341. Dieser hat einen Bildspeicher mit 240 x 320 x 18 Bits. Mit der GFX-Bibliothek werden für dieses Display davon nur 240 x 320 x 16 Bits genutzt.
Link zum Produkt
Pins auf der Display Seite von links nach rechts:
VCC GND CS RES RS SDA SCK LED (Rest für Touchscreen)
Die Betriebsspannung an VCC und an den Eingängen kann von 3.3V bis 5V betragen.
Die Spannung für die Hintergrundbeleuchtung am Pin LED muss 3.3V betragen.
Das Display hat zusätzlich einen Touchscreen.
Dieses Display nutzt den Treiberbaustein ILI9341. Dieser hat einen Bildspeicher mit 240 x 320 x 18 Bits. Mit der GFX-Bibliothek werden für dieses Display davon nur 240 x 320 x 16 Bits genutzt.
Link zum Produkt
Pins auf der Display Seite von links nach rechts:
VCC GND CS RES RS SDA SCK LED (Rest für Touchscreen)
Die Betriebsspannung an VCC und an den Eingängen kann von 3.3V bis 5V betragen.
Die Spannung für die Hintergrundbeleuchtung am Pin LED muss 3.3V betragen.
Das Display hat zusätzlich einen Touchscreen.
____________________________________________________________
Alle Displays funktionieren mit 3.3 V. Bei 5V sind teilweise Pegelwandler erforderlich.
Alle Displays können mit der Grafik-Bibliothek „GFX Library for Arduino“ angesteuert werden.
Da man für grafische Anwendungen und Bilder meist viel Speicherplatz benötigt, werde ich für die weiteren Betrachtungen Mikrocontroller der Typen ESP8266 und ESP32 heranziehen. Natürlich können diese Displays auch mit anderen Mikrocontrollern oder Raspberry Pi verwendet werden.
Hier noch einmal die verwendeten Pins und die Verbindung zum Mikrocontroller:
Pin |
Funktion |
and. Bez. |
ESP8266 |
ESP32 |
SDA |
Daten vom MC zum Display |
MOSI, SDI |
GPIO13 / D7 |
GPIO23 |
SCL |
Takt für den SPI-Bus |
SCK |
GPIO14 / D5 |
GPIO18 |
RS |
Unterscheidung Kommando oder Daten |
A0, DC, |
GPIO15 / D8 |
GPIO05 |
CS |
Chipselect |
|
GPIO16 / D0 |
GPIO04 |
RES |
Reset Eingang, muss nicht verwendet werden |
RESET, RST |
GPIO00 / D3 |
GPIO00 |
Statt der sonst sehr beliebten Adafruit GFX Bibliothek wird in diesem Beitrag Die „GFX Library for Arduino“ von „Moon on our Nation“ beschrieben. Sie bietet zusätzliche Grafikfunktionen für Ellipse und Kreissegment. Sie unterstützt neben den Adafruit-GFX Schriften auch die u8g2 Schriften.
Die Bibliothek unterstützt alle hier beschriebenen Displays und noch viele mehr.
Programmrumpf enthält Pinbelegung und Initialisierung:
#include "SPI.h"
#include <Arduino_GFX_Library.h>
// Pins for SPI bus
#define TFT_CLK 18 //SPI Clock
#define TFT_DA 23 //SPI Data (MOSI)
#define TFT_DC 5 //Data/Command
#define TFT_CS 4 //Chip select
#define TFT_RST 0 //Reset
Arduino_DataBus *bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS,
TFT_CLK, TFT_DA);
****** Diese Zeile ist für jedes Display individuell ******
void setup() {
tft->begin();
}
void loop() {
}
Die fehlende Zeile erzeugt den Zeiger auf ein vom Display abhängiges GFX-Objekt. Der Konstruktor hat für jedes Display dieselben Parameter aber natürlich mit unterschiedlichen Werten.
Arduino_XXXX(Bus, ResetPin, Rotation, IPSmode, Breite, Höhe, x1Ofs, y1Ofs, x2Ofs,y2Ofs, RGB)
Bus |
Zeiger auf das in der Zeile davor erzeugte Objekt, das den Datenaustausch definiert. |
ResetPin |
Pin-Nummer für den Reset Pin oder -1 wenn der Reset Pin nicht verwendet wird. |
Rotation |
Definiert die Ausrichtung des Bildschirms. Die Werte 0, 1, 2 und 3 sind möglich. |
IPS-Mode |
Definiert, ob ein Display IPS (In Panel Switching) nutzt oder nicht. Werte true oder false. |
Breite |
Die Breite des Displays in Pixel. |
Höhe |
Die Höhe des Displays in Pixel. |
x1Ofs |
Korrekturwert, um Unterschiede zwischen Displays auszugleichen. |
Y1Ofs |
Korrekturwert, um Unterschiede zwischen Displays auszugleichen. |
X2Ofs |
Korrekturwert, um Unterschiede zwischen Displays auszugleichen. |
Y2Ofs |
Korrekturwert, um Unterschiede zwischen Displays auszugleichen. |
RGB |
Farbanordnung. Wenn der Wert false ist, ist die Anordnung BGR. |
Die fehlende Zeile für die verschiedenen Displays:
ST7735:
Arduino_GFX *tft = new Arduino_ST7735(bus, TFT_RST, 0, false ,128,160,2,1,2,1,false);
ST7789:
Arduino_GFX *tft = new Arduino_ST7789(bus, TFT_RST, 2, true,240,280,0,20,0,20);
GC9A01A:
Arduino_GFX *tft = new Arduino_GC9A01(bus, TFT_RST, 0, true);
ILI9341:
Arduino_GFX *tft = new Arduino_ILI9341(bus, TFT_RST, 0, false);
Wenn bei manchen Displays nicht alle Parameter angegeben wurden, heißt das, dass die Default-Parameter verwendet werden können.
Die GFX-Bibliothek enthält Kommandos zum Ausgeben von Grafik und Text auf ein Display.
Der Nullpunkt ist links oben.
Farben werden in Computersystemen üblicherweise mit 24 Bits dargestellt. Dabei sind 8 Bits für rot, 8 Bits für grün und 8 Bits für blau. Insgesamt ergibt das 16 Millionen verschiedene Farben. Da die Controller für die TFT-Displays einen Bildspeicher integrieren müssen, wollte man Speicherplatz einsparen und verwendet daher 18 Bits, also jeweils 6 Bits für rot grün und blau. Das ergibt insgesamt 260000 Farben. Für die Ansteuerung aus byteorientierten Systemen sind 18 Bits aber unpraktisch. Man hat daher ein weiteres Format definiert, das mit 16 Bits auskommt. Es hat jeweils 5 Bits für rot und blau, sowie 6 Bits für grün. Die Anzahl der Farben wird dadurch auf 65000 reduziert. Die GFX-Bibliothek arbeitet mit diesem Format.
Um die Verwendung zu vereinfachen, werden in den Bibliotheken zu den verschiedenen Treibern jeweils einige Farben mit Namen definiert. Flexibler ist es aber, selbst ein solches Headerfile mit Farbdefinitionen zu erstellen, welches man dann einfach inkludieren kann.
Um die Definition der Farben einfach visuell vornehmen zu können, habe ich eine Windows-Anwendung erstellt, die neben anderen Funktionen zur Anpassung von Bildern und Schriften auch Farbpaletten erstellen und als Headerfile abspeichern kann. Die Anwendung heißt GFX-Tool und kann von GIT-Hub heruntergeladen werden. Dort findet sich auch eine detaillierte Anleitung.
Mit der Funktion
void setRotation(uint8_t rotation);
kann die Ausrichtung geändert werden. Der Parameter rotation kann die Werte 0, 1, 2 oder 3 annehmen.
Da sich Breite und Höhe abhängig von der Ausrichtung ändern, sollte man, nachdem die Ausrichtung geändert wurde, Breite und Höhe mit den Funktionen
uint16_t width(); und uint16_t height();
ermitteln.
Mit der Funktion
void fillScreen(uint16_t color);
wird der gesamte Bildschirm mit der angegebenen Farbe gefüllt.
Die einfachste Grafikfunktion ist
Void drawPixel(int16_t x, int16_t y, uint16_t color);
Der Bildpunkt an der angegebenen Position, wird in der angegebenen Farbe dargestellt.
Mit der Funktion
void drawLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color);
wird eine Linie von Position x1, y1 nach x2, y2 in der angegeben Farbe gezeichnet.
Will man nur horizontale oder vertikale Linien zeichnen, sollte man die folgenden Funktionen verwenden.
void drawFastHLine(int16_t x1, int16_t y1, int16_t length, int16_t color);
void drawFastVLine(int16_t x1, int16_t y1, int16_t length, int16_t color);
Hier werden als Parameter der Startpunkt und die Länge in Pixel angegeben.
Mit der Funktion
void drawRect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t color);
wird an der Position x, y ein Rechteck ausgegeben und mit der Funktion
void fillRect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t color);
wird das Rechteck mit der angegebenen Farbe gefüllt.
Mit der Funktion
void drawCircle(int16_t x, int16_t y, int16_t radius, uint16_t color);
wird mit Mittelpunkt x, y ein Kreis ausgegeben und mit der Funktion
void fillCircle(int16_t x, int16_t y, int16_t radius, uint16_t color);
wird der Kreis mit der angegebenen Farbe gefüllt.
Mit der Funktion
void drawRoundRect(int16_t x, int16_t y, int16_t width, int16_t height, int16_t radius, uint16_t color);
wird an der Position x, y ein Rechteck mit abgerundeten Ecken ausgegeben und mit der Funktion
void fillRoundRect(int16_t x, int16_t y, int16_t width, int16_t height, int16_t radius, uint16_t color);
wird das Rechteck mit der angegebenen Farbe gefüllt.
Mit der Funktion
void drawTriangle(int16_t x1, int16_t y1, int16_t x2, int16_t y2, int16_t x3, int16_t y3, uint16_t color);
wird an der Position x, y ein Dreieck ausgegeben und mit der Funktion
void fillTriangle(int16_t x1, int16_t y1, int16_t x2, int16_t y2, int16_t x3, int16_t y3, uint16_t color);
wird das Dreieck mit der angegebenen Farbe gefüllt.
Mit der Funktion
void drawEllipse(int16_t x, int16_t y, int16_t r1, int16_t r2, uint16_t color);
wird mit Mittelpunkt x, y eine Ellipse mit dem x-Radius r1 und dem y-Radius r2 ausgegeben. Mit der Funktion
void fillEllipse(int16_t x, int16_t y, int16_t r1, int16_t r2, uint16_t color);
wird die Ellipse mit der angegebenen Farbe gefüllt.
Mit der Funktion
void drawArc(int16_t x, int16_t y, int16_t r1, int16_t r2, float a1, float a2, uint16_t color);
wird mit Mittelpunkt x, y ein Kreissegment mit dem äußeren Radius r1 und dem inneren Radius r2 ausgegeben. Das Segment beginnt mit dem Winkel a1 und endet mit dem Winkel a2. Der Winkel läuft im Uhrzeigersinn. 0 Grad entspricht der positiven x-Ache. Mit der Funktion
void fillArc(int16_t x, int16_t y, int16_t r1, int16_t r2, float a1, float a2, uint16_t color);
wird das Kreissegment mit der angegebenen Farbe gefüllt.
Zur Textausgabe steht ein interner Zeichensatz mit 5 x 7 Pixel in einem Feld von 6 x 8 Pixel zur Verfügung. Die Zuordnung Zeichen zu Code erfolgt nach Codepage 437. Das heißt für Sonderzeichen außerhalb des ASCII-Codes muss eine Umrechnung erfolgen.
Zur Textausgabe sind mehrere Funktionen vorhanden. Mit der Funktion
void setCursor(int16_t x, int16_t y);
wird die Anfangsposition gesetzt. Die Funktionen
void setTextColor(uint16_t c);
und
void setTextColor(uint16_t c, uint16_t bg);
setzen die Textfarbe und in der zweiten Variante eine Farbe für den Hintergrund. Die Funktion
void setTextSize(uint8_t s);
setzt die Textgröße. Es handelt sich dabei um einen Multiplikator. Default ist Text Size = 1 gesetzt. Setzt man Text Size auf 2, bestehen alle Zeichen aus 10 x 14 Pixel statt 5 x 7. Mit der Funktion
void setTextWrap(bool w);
kann man festlegen, ob beim Erreichen des rechten Bildschirmrandes eine neue Zeile begonnen werden soll.
Die Textausgabe selbst erfolgt mit den bekannten Funktionen print() oder println().
Die folgende Funktion nutzt zur Demonstration alle Grafikfunktionen. Sie ist so ausgelegt, dass auf jedem Display das gesamte Bild zu sehen ist.
void showTest() {
tft->fillScreen(WHITE);
tft->fillRoundRect(5,5,110,110,10,CYAN);
tft->drawRoundRect(5,5,110,110,10,PURPLE);
tft->fillRect(10,17,30,30,YELLOW);
tft->drawRect(10,17,30,30,BLACK);
tft->fillTriangle(45,47,60,17,75,47,RED);
tft->drawTriangle(45,47,60,17,75,47,WHITE);
tft->fillCircle(95,32,15,GREEN);
tft->drawCircle(95,32,15,BLUE);
tft->drawLine(10,60,110,60,MAROON);
tft->fillEllipse(40,92,30,15,ORANGE);
tft->drawEllipse(40,92,30,15,NAVY);
tft->fillArc(90,92,15,5,180,360,MAGENTA);
tft->drawArc(90,92,15,5,180,360,YELLOW);
tft->setTextColor(BLACK);
tft->setTextWrap(true);
tft->setTextSize(1);
tft->setCursor(5, 120);
tft->print("AZ-Delivery");
}
Das Ergebnis sollte so aussehen.
Das vollständige Testprogramm kann auch heruntergeladen werden.
Im zweiten Teil wird gezeigt, wie man mit externen Schriften und mit Bildern arbeitet.
]]>Heute funkt es, oder besser funkt er. Der ESP32 bekommt heute ein RTC-BOB spendiert, mit dessen Hilfe er die Dateinamen bei der Erfassung von Bodenerschütterungen zeitlich dingfest machen kann. Das Board mit einem DS3231 weist eine sehr gute Ganggenauigkeit auf und wird zudem jeden Tag zu einer bestimmten Zeit, die Sie nach Ihren Wünschen festlegen können, durch Kontakt mit einem Time-Server im Web synchronisiert. Das geschieht auf der Grundlage des NTP (Network Time Protocol). Der DS3231 selbst gibt via Interrupt den Auftrag, die Synchronisation durchzuführen. Das neue Format der Dateinamen Datum_Zeit_data.csv macht die bisherige, einfache Durchnummerierung obsolet. Das alles erfordert den Umbau einiger Funktionen, bringt neue und macht eine überflüssig. Folgen Sie mir auf eine neue Tour durch
heute
Gleich zu Beginn will ich Ihnen die beiden Störenfriede des Projekts offenbaren. Es begann bereits mit dem Einführen des Card-Readers in der letzten Folge. Hin und wieder streikte das Mounten, also das Einhängen der Karte in den Verzeichnisbaum des Dateisystems, meistens funktionierte es und die Karte war ansprechbar, manchmal aber eben nicht. Da kam mir dann wieder die Belegung der GPIO-Pins beim Booten in den Sinn.
Tatsächlich hatte ich die CS-Leitung an GPIO5 liegen, so wie ich es in einer früheren Schaltung gemacht hatte. Nach dem Umzug auf GPIO4 klappte es zuverlässig, die Karte einzuhängen, bis - ja, bis zum Beginn der zweiten Erweiterung.
Ich hatte alle Programmbausteine aus meiner Sammlung zusammengetragen und angepasst. Das Einhängen der SD-Karte wurde gemeldet, die anderen Objekte wurden ohne Fehler deklariert, doch als die gesamten Vorbereitungen durchgelaufen waren und ich die Karte ansprechen wollte, war sie verschwunden. Eine erneute Verlegung der CS-Leitung half nichts. Ich tauschte den Card-Reader, die Speicherkarte - nada! Also holte ich mir das e-Book zum Modul. Leider wieder kein Hinweis.
Da hilft nur strategisches Vorgehen. Zum Debuggen gibt es ein prima Hilfsmittel: sys.exit(). Mit dieser Funktion kann man den Programmlauf an genau definierten Stellen abbrechen. Wir haben das ja schon etliche Male praktiziert. Ich überlasse dem Programm die Aufgabe, Objekte einzurichten und Variablen zu initiieren. Alles was hier über drei Zeilen hinausgeht, ist mir händisch zu umständlich. Nachdem das Programm gestoppt hat, kann ich meine Tests und Abfragen durchführen. Das brachte mich hier bis an die Stelle an der connect() aufgerufen wird, um zum WLAN-Router zu verbinden. Bis hierher war die SD-Karte ansprechbar, nach dem Aufruf nicht mehr. connect() war also der Ganove, der mir die Karte geklaut hatte. Und zwar verschwand die Karte in dem Moment, als sich der ESP32 mit
nic.connect(mySSID, myPass)
beim Router anzumelden versuchte. Klick, machte es bei mir, das ist die Ursache. Klar! Die Vcc-Leitung des Card-Raders hatte ich an den 3,3V-Pin des ESP32 gelegt. Für den Normalbetrieb reichte das auch aus. Aber sobald der Controller zu funken begann, fiel durch den erhöhten Strombedarf des ESP32 die Spannung so weit ab, dass der Kartenleser einen Neustart machte und die Karte somit nicht mehr gemountet war. Seit dem liegt Vcc am Vin-Pin mit 5V und der Card-Reader schnurrt wie eine zufriedene Mietzekatze. Ich hoffe, Ihnen mit solchen Geschichten zur Fehlersuche und Beseitigung immer wieder ein paar Tipps zum Vorgehen in eigenen Problemlagen geben zu können.
Die Teileliste umfasst auch die Hardware von der vorangegangenen Folge. Ergänzt habe ich nur das RTC-Modul. Letzteres ist am I2C-Bus anzuschließen und verträgt sich gut mit dem Display, weil alle drei andere Hardwareadressen (HWADR) haben. Ja, Sie haben schon richtig gelesen, und ich kann auch richtig zählen, drei. Auf dem Board befindet sich neben dem DS3231 nämlich noch ein 4-MB-Flash-EEPROM. Im MicroPython-Modul ds3231.py wohnt neben der Klasse DS3231 noch deren Nachbar AT24C32. Mit den Methoden aus dieser Klasse können Sie Integerzahlen als Word (=2 Byte), strings und Blobs (=Bytesfolgen und Bytearrays) speichern und abrufen.
Der Anschluss SQW des DS3231 ist als IRQ-Pin zu gebrauchen. Ich verbinde diesen Ausgang mit dem GPIO26 und kann dadurch Programmunterbrechungen beim ESP32 auslösen.
1 |
oder ESP32 NodeMCU Module WLAN WiFi Development Board oder NodeMCU-ESP-32S-Kit |
1 |
GY-61 ADXL335 Beschleunigungssensor 3-Axis Neigungswinkel Modul |
1 |
|
1 |
|
1 |
Mehrgang rotary Potentiometer mit Schutzwiderstand 3590S 10K Ohm |
1 |
|
1 |
SPI Reader Micro Speicher SD TF Karte Memory Card Shield Modul |
1 |
Micro-SD-Card, 4GB – 32GB |
1 |
|
1 |
LED |
1 |
Widerstand 270 Ohm |
diverse |
Jumperkabel |
|
Digital-Voltmeter (DVM) für die Kalibrierung |
Die Anordnung der Teile zeigt Abbildung 1. Der Card-Reader steckt links oben, daneben der DS3231.
Abbildung 1: Seismometer - zweite Ausbaustufe
Fürs Flashen und die Programmierung des ESP32:
Thonny oder
Python 3.8 oder höher incl. Idle IDE für Python auf dem PC
ssd1306.py Hardwaretreiber für das OLED-Display
oled.py API für OLED-Displays
sdcard.py Treiber für das SD-Reader-Modul
buttons.py API für den Betrieb von Tasten
earthquake_sd.py Betriebsprogramm
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 05.02.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Abbildung 2: Seismometer - Schaltung mit RTC
Direkteingaben im Terminal von Thonny (REPL) sind im Text fett formatiert und am Prompt >>> zu erkennen. Antworten vom ESP32 sind kursiv gesetzt.
Auf dem I2C-Bus findet ein Scan in dieser Schaltung drei Hardware-Adressen.
>>> from machine import Pin, SoftI2C
>>> i2c=SoftI2C(scl=Pin(22),sda=Pin(21),freq=400000)
>>> i2c.scan()
[60, 87, 104]
Die 60 = 0x3C gehört dem Display, die 87= 0x57 dem EEPROM und der RTC-Chip wohnt in Hausnummer 104=0x68.
Um den Post im üblichen Rahmen zu halten werde ich hier nur die Teile des Programms besprechen, die geändert wurden, oder neu hinzugekommen sind. Die beiden bisher erschienen Beiträge, auf denen der aktuelle aufbaut, finden Sie hier (Teil 1) und hier (Teil 2).
Die erweiterte Infrastruktur macht natürlich Ergänzungen und Änderungen an der Software nötig. Das beginnt beim Importgeschäft mit den fett formatierten Zeilen. Wir haben ein paar zeitliche Klimmzüge vor, localtime, ds3231 und ntptime helfen uns dabei. Für die Netzwerkverbindung sorgen network und socket.
from machine import Pin, ADC, SoftI2C, freq, SPI
from time import sleep,ticks_ms, localtime
import os, sys, sdcard
from oled import OLED
from esp32 import NVS
from buttons import *
import network, socket
import ntptime as ntp
from ds3231 import DS3231
Damit der Router den ESP32 reinlässt, geben Sie hier bitte Ihre eigenen Credentials ein. In receiver steht an erster Stelle die IP-Adresse des Geräts, mit dem Sie die Daten vom ESP32 empfangen wollen. Bei mir ist das ein Windows-Rechner, auf dem Python 3.8 läuft. Die IDE Idle ist darauf automatisch mit eingerichtet. Das brauche ich später zum Editieren des UDP-Empfangsprogramms. An der zweiten Position des Tupels steht eine frei wählbare Portnummer, die auch im Empfangsprogramm angegeben werden muss.
receiver=("10.0.1.10",9091)
# Geben Sie hier Ihre eigenen Zugangsdaten an
mySSID = 'EMPIRE_OF_ANTS'
myPass = 'nightingale'
Der DS3231 ist darauf getrimmt, eine Zeitsynchronisation anzufordern, wenn sein Alarm-Timer 2 abgelaufen ist. Das geschieht über dessen Ausgangspin SQW, das in Ruhezustand auf HIGH-Pegel liegt und das dann nach LOW wechselt. Damit der ESP32 darauf reagieren kann, muss einer der GPIO-Pins als interruptfähiger Eingang programmiert werden und bei fallender Flanke eine Interrupt-Service-Routine (ISR) aktivieren. Der print-Befehl in isr() kann auch wegfallen. Er sagt mir in der Entwicklungsphase nur, dass die Routine ausgeführt wurde. Die Methode ClearAlarm() aus der Klasse DS3231 setzt das IRQ-Bit für den Alarm 2 zurück, damit geht SQW wieder auf HIGH. synchronize() kontaktiert dann den NTP-Server pool.ntp.org und holt die Anzahl der Sekunden seit Anfang der Epoche, dem 01.01.2000 0:0:0. Das war ein Samstag und der erste Tag im Jahr. Die folgende Anweisung zeigt das.
>>> localtime(0)
(2000, 1, 1, 0, 0, 0, 5, 1)
def isr(pin):
print("Pin:",pin)
ds.ClearAlarm(2)
synchronize()
ds=DS3231(i2c)
rtcirq=Pin(26,Pin.IN)
rtcirq.irq(handler=isr,trigger=Pin.IRQ_FALLING)
Dem Konstruktor der Klasse DS3231 übergebe ich das I2C-Objekt. GPIO26 wird als Eingang geschaltet, damit der IRQ durch den Pegel auf der SQW-Leitung reagieren kann. Würde ich den Pin als Ausgang schalten, dann würde der IRQ durch einen Pegelwechsel des Ausgangspuffers ausgelöst. Die letzte Zeile macht die Unterbrechungsanforderung scharf. In handler übergebe ich die Referenz auf die ISR.
Die Zeilen 33 bis 173 in earthquake_rtc+wlan.py wurden nicht geändert, ich überspringe sie daher.
Die nächste Änderung erfolgte in der Routine readData() in Bezug auf die neuen Dateinamen. Die Funktion liest eine Datei ein und füllt damit die global definierte Liste s. In der neuen Dateistruktur wird auch der jeweilige Ruhewert vom ADXL335 als erster Eintrag mit abgelegt. Damit er nach dem Einlesen auch global verfügbar ist, sind s und S0 global deklariert. Nach dem Leeren der Liste s setze ich den Pfad zur Datei zusammen, deren Name an den Parameter n übergeben wird.
def readData(n):
global s,s0
s=[]
name="/sd/"+n
try:
with open(name,"r") as f:
line=f.readline()
zeile=line.strip()
s0=int(zeile)
for line in f:
zeile=line.strip()
if zeile.find(";") != -1:
nr,s1=zeile.split(";")
val=int(s1)
s.append(val)
except OSError as e:
print("Datei nicht gefunden",e)
d.writeAt("data not found",0,5)
sleep(3)
d.clearAll()
try fängt Fehler ab, die zum Beispiel eine nicht vorhandene Datei verursachen könnte. Ich öffne die Datei zum Lesen. Über das Handle f habe ich jetzt Zugriff auf den Inhalt. Ein Dateihandle ist von der Funktion her vergleichbar mit einem Socket im Funkverkehr. Beides kann als Schleusentor zu einem (Informations-) Kanal verstanden werden.
Die erste Zeile wird gelesen und vom LF (= Linefeed = \n") befreit. Danach muss der String in eine Zahl umgewandelt werden, damit man damit rechnen kann. Die for-Schleife holt jetzt die restlichen Zeilen aus der Datei. LF entfernen und dann prüfen wir auf einen Strichpunkt, der in der Zeile die Nummer vom Wert trennt. Wird er nicht gefunden, dann ist der Rückgabewert von find() -1 und die Zeile wird übersprungen. Sonst gibt find die Position des Strichpunkts zurück, die wir aber nicht verwenden, denn es gibt eine komfortablere Methode für die Trennung, split(). split() liefert eine Liste, die sogleich in nr und s1 entpackt. s1 bauen wir in eine Zahl um und append() hängt diese an die Liste s an.
Eine nicht auffindbare Datei verursacht eine OSError-Exception, die wir mit except abfangen und dann Meldung im Terminal und im Display machen.
Im gleichen Zug mit dem Ändern der Lese-Routine muss auch die Funktion zum Speichern von Werten angepasst werden. Als erstes bestimmen wir die Länge der an v übergebenen Liste. Das kann, wie in diesem Programm, die gesamte Liste s sein, oder auch nur ein Slice, also ein Teil davon. len() bestimmt die Länge der Liste, also die Anzahl an Werten. Die Funktion name() bastelt aus dem aktuellen Timestamp den Dateinamen.
>>> name()
'2023-02-27_10-09-15_data.csv'
Den teilen wir an den Unterstrichen auf. Die ersten beiden Listenelemente enthalten Datum und Uhrzeit, das letzte Element schicken wir ins Nirwana. Die Liste entpacken wir sofort nach datum und zeit. Das Nirwana wird vertreten durch den "_". Der Unterstrich ist quasi die notwendige dritte Variable
>>> name().split("_")
['2023-02-27', '10-12-07', 'data.csv']
Der Dateipfad wird zusammengesetzt und der Basiswert der aktuellen Session aus dem nichtflüchtigen Speicher geholt. Im Display und im Terminal wird der Vorgang zur Kenntnis gebracht. Der Backslash kann lange Zeilen überall dort umbrechen, wo auch ein Leerzeichen stehen darf.
def save2sd(v):
aw=len(v)
dn=name()
datum,zeit,_=dn.split("_")
file="/sd/"+dn
S0=nvs.get_i32("s0")
d.clearAll()
d.writeAt("SAVING file",0,2)
d.writeAt(datum,0,3)
d.writeAt(zeit,0,4)
print("***** Speichere {} Werte auf SDCard *****".\
format(aw))
with open(file,"w") as f:
f.write("{}\n".format(S0))
for i in range(aw):
f.write("{};{}\n".format(i,v[i]))
print("============= FERTIG ================")
d.writeAt("DONE!",0,5)
sleep(3)
d.clearFT(0,2)
Wir öffnen die Datei zum Schreiben und sichern erst einmal S0, LF, "\n", nicht vergessen, sonst kann man den Bandwurm beim Einlesen nicht mehr trennen. In eine Textdatei können auch nur Strings geschrieben werden. Die Umcodierung von Zahl nach String erledigen hier der Formatstring "{}\n" und die String-Methode format(). Folgende Anweisungen sind wirkungsgleich.
>>> "{}\n".format(S0)
>>> Str(S0)+"\n"
Die for-Schleife schreibt die Nummer-Wert-Paare als String mit einem LF am Ende in die Datei. Vollzugsmeldung ins Terminal und Display, drei Sekunden zum Lesen, dann löschen wir im OLED-Display alles von Zeile 2, Spalte 0 bis Zeile 5, Spalte 15.
Mit Formatstrings habe ich aber weitere Möglichkeiten, das Ausgabebild zu beeinflussen. Ein paar Beispiele:
>>> a=3.141593
>>> str(a)
'3.141593'
>>> "pi = {0:.2f}".format(a)
'pi = 3.14'
>>> b=0xe43c
>>> b
58428
>>> "{0:#X}".format(b)
'0XE43C'
>> "0b{0:024b}".format(b)
'0b000000001110010000111100'
>>> c=31
>>> "0b{0:024b}".format(c)
'0b000000000000000000011111'
Im Menü ergänzen wir einen Programmpunkt zum Speichern der Werteliste auf der SD-Karte.
def Menu():
while 1:
d.clearAll(False)
d.writeAt("MENU",0,0,False)
d.writeAt("D Dateien",0,1,False)
d.writeAt("A LISTE SENDEN",0,2,False)
d.writeAt("C EXIT",0,5)
waitForRelease(keyA,2)
t=waitForAnyKey((keyD,keyA,keyC),5000)
if t==0:
waitForRelease(keyD,2)
file=files()
elif t==1:
try:
print(file)
except:
file=name()
sendList(s,file)
elif t==2:
d.clearAll()
return
Trickreich prüfe ich mit try, ob bereits ein Dateiname in file existiert. Das ist der Fall, wenn der print-Befehl keine Exception wirft, except wird dann übersprungen. Existiert noch keine Variable file, weil ich zum Beispiel vorher keine Datei via Taste D eingelesen habe, sondern direkt von einer Messung komme, dann muss ich einen Namen aus dem aktuellen Timestamp durch name() generieren lassen. Denn würde file nicht existieren, dann gibt das einen Fehler, wenn ich file als Argument beim Aufruf von sendList() referenziere. file existiert somit in jedem Fall und hat den Timestamp einer eingelesenen Liste oder den der aktuellen Messung.
Dateinamen spielen auch eine Rolle in files(). Auch hier stehen demnach Änderungen an. Der äußere Ablauf bleibt derselbe, Die Anzeige der Dateinamen muss wegen deren Länge wieder auf zwei Zeilen im Display aufgeteilt werden. Das passiert in der schon bekannten Weise.
def files():
d.clearAll()
dateien=os.listdir("/sd")[1:]
n=len(dateien)
dateien.sort()
i=0
while 1:
dn=dateien[i]
datum,zeit,_=dn.split("_")
file="/sd/"+dn
d.writeAt(datum,0,0)
d.writeAt(zeit,0,1)
x,y=joystick(5)
Im Bereich E Datei löschen habe ich noch einen Bug erkannt und beseitigt. Wenn keine Datei mehr existiert (n ist 0), weil alle gelöscht wurden, macht es auch keinen Sinn, einen Namen anzuzeigen, oder gar die Datei zu laden. Also, go back to caller, zurück zum aufrufenden Programm.
Sonst war es offenbar möglich, die noch angezeigte Datei zu entfernen, wir melden also, "FILE KILLED". Der Index i in der Liste der Dateinamen dateien hat auch nach dem Löschen immer noch denselben Wert, zeigt aber jetzt nach dem Entfernen eines Eintrags (del dateien[i]) auf den nächsten. Wie geht das? Nun, wenn Sie auf ein Buch in einem Stapel zeigen und dieses herausziehen, rutscht das darüber liegende nach unten, Ihr Finger zeigt immer noch auf dieselbe Stelle, an der jetzt ein anderes Buch liegt.
if getTouch(confirm): # E Datei loeschen
os.remove(file)
del dateien[i]
n=len(dateien)
if n == 0 :
d.writeAt("NO FILES LEFT ",0,5)
sleep(3)
d.clearAll()
return
else:
d.writeAt("FILE KILLED ",0,5)
if n==i and i>0:
i-=1
else:
return
sleep(3)
d.clearFT(0,5)
Da ist aber noch ein zweites Grenzwertproblem. Was passiert, wenn Sie auf das oberste Buch im Stapel zeigen und dieses herausziehen? Dann zeigt Ihr Finger ins Leere. Der Interpreter von MicroPython meldet dann Index out of range.
>>> w=[1,2,3]
>>> w[2]
3
>>> w[3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
Wenn Sie weiterhin auf ein Buch zeigen wollen, müssen Sie jetzt wohl oder übel Ihren Finger um eine Position absenken, i-=1 oder i = i - 1. Das geht natürlich nur, wenn nach unten noch Bücher liegen, also wenn i größer 0 ist. Falls i bereits 0 ist, haben Sie von oben kommend eben das letzte Buch entfernt. Es gibt nix mehr zu holen und auch nicht anzuzeigen, also, get back to sender, return.
Jetzt kommen lauter neue Funktionen. Beginnen wir mit dem Aufbau einer Verbindung zum WLAN-Router. Die Funktion connect() tötet erst einmal das Accesspoint-Interface, wenn es denn existiert. Das ist zwar beim ESP32 in der Regel nicht aktiv und daher kein Problem, verursacht dieses aber beim ESP8266 sehr häufig.
Dann wird das Station-Interface-Objekt erzeugt und aktiviert. Wir lassen uns von config() die MAC-Adresse flüstern und wandeln das, meistens kryptische, bytes-Objekt durch die Funktion hexMac() in normale Hexadezimal-Ziffern 0-9 und a bis f.
STATION MAC: fc-f5-c4-27-9-10
def connect():
# ************** Zum Router verbinden *******************
nic=network.WLAN(network.AP_IF)
nic.active(False)
nic = network.WLAN(network.STA_IF) # erzeugt WiFi-Objekt
nic.active(True) # nic einschalten
MAC = nic.config('mac')# binaere MAC-Adresse abrufen und
myMac=hexMac(MAC) # in Hexziffernfolge umwandeln
print("STATION MAC: \t"+myMac+"\n") # ausgeben
Wir sollten jetzt dem Interface noch etwas Zeit geben, sich zu konstituieren. Interne WLAN-Errors können sonst die Folge sein. Dann starten wir mit nic.connect() und den Credentials den Verbindungsversuch, wenn noch keine Connection besteht. In der while-Schleife prüfen wir den Status der Verbindung. Ist der Rückgabewert von status() noch nicht 1010, müssen wir noch warten, denn das bedeutet, dass uns vom DHCP-Server im Netz noch keine IP-Adresse zugewiesen wurde. In der Regel wird der Vergabeservice für IP-Adressen auf dem WLAN-Router laufen. Ohne diesen Dienst kann unser Programm nicht funktionieren, es sei denn, wir vergeben mit ifconfig() selbst eine IP.
nic.ifconfig(('192.168.0.4', '255.255.255.0', '192.168.0.1', '8.8.8.8'))
Während der ESP32 auf den Abschluss der Verbindung wartet, wird im Sekundenabstand im Display und im Terminal ein Punkt ausgegeben.
sleep(1)
if not nic.isconnected():
nic.connect(mySSID, myPass)
d.writeAt("WLAN connecting",0,1)
points="............"
n=1
while nic.status() != network.STAT_GOT_IP:
print(".",end='')
d.writeAt(points[0:n],0,2)
n+=1
sleep(1)
Wir lassen uns den Verbindungsstatus mitteilen und bekommen IP, Netzwerkmaske und Gateway mitgeteilt. Damit wir auch händisch Versuche mit der Netzwerkverbindung starten können, lassen wir uns eine Referenz auf die WLAN-Instanz zurückgeben.
Status: STAT_GOT_IP
STA-IP: 10.0.1.220
STA-NETMASK: 255.255.255.0
STA-GATEWAY: 10.0.1.20
print("\nStatus: ",connectStatus[nic.status()])
d.clearAll()
STAconf = nic.ifconfig()
print("STA-IP:\t\t",STAconf[0],"\nSTA-NETMASK:\t",\
STAconf[1], "\nSTA-GATEWAY:\t",STAconf[2] ,sep='')
print()
d.writeAt(STAconf[0],0,0)
d.writeAt(STAconf[1],0,1)
d.writeAt(STAconf[2],0,2)
return nic
Ein Aufruf von connect() sieht dann etwa so aus.
>>> sta=connect()
STATION MAC: fc-f5-c4-27-9-10
Status: STAT_GOT_IP
STA-IP: 10.0.1.220
STA-NETMASK: 255.255.255.0
STA-GATEWAY: 10.0.1.20
>>> sta
<WLAN>
>>> sta.status()
1010
>>> sta.ifconfig()
('10.0.1.220', '255.255.255.0', '10.0.1.20', '10.0.1.100')
sta verweist auf die Instanz nic und ist unser Informationskanal. Damit Informationen fließen können, brauchen wir noch ein Tor, das wir zum richtigen Zeitpunkt öffnen können, einen Socket. Das Protokoll meiner Wahl ist UDP (User Datagram Protocol). Es ist schnell, arbeitet ohne Handshakes beim Verbindungsaufbau, allerdings auch auf niedrigem Absicherungsniveau was Datenintegrität und Datenverlust angeht. Damit kann man in unserem Anwendungsfall leben.
def setSocket():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sock.bind(('', 9003))
print("sending on port 9003...")
sock.settimeout(0.1)
return sock
Ein socket-Objekt sock aus der Familie ipv4 wird für den Datagram-Transport instanziiert. SO_REUSEADDRESS lässt uns, ohne den ESP32 neu booten zu müssen, dieselbe IP-Adresse wiederverwenden. Dann binden wir die Portnummer 9003, oder eine andere Nummer größer als 1024, an die uns zugewiesene IP-Adresse.
Der durch settimeout() gesetzte Zeitraum von 0,1 Sekunden beendet das Lauschen der Empfangs- oder Sendeschleife und verhindert so eine Blockade der Mainloop, die beides bedienen muss. Damit wir für das Senden von Daten auf unseren Socket zurückgreifen können, muss setSocket() das Objekt sock zurückgeben.
Mit der nächsten Funktion wird nun endlich gesendet, sendList(). Speicher aufräumen ist wichtig, denn es werden einige kB umgeschaufelt. Dann öffnen wir den Socket und lassen uns sagen, was grade abgeht. Wir holen den aktuellen Basiswert der letzten Messung, s0, aus dem NVS und bereiten daraus einen String mit führendem ";". Dieser String S0 wird an jede Zeile aus Nummer und Wert angehängt und liefert später, in der grafischen Auswertung von EXCEL, die Nulllinie. Als Erstes senden wir den Dateinamen, damit das Empfangsprogramm die Datei unter derselben Bezeichnung wie auf der SD-Karte ablegen kann. Die for-Schleife räumt bei jedem Durchlauf auf, setzt dann die Zeile zusammen und sendet sie über den Socket. Die Zeile mit dem Inhalt ende sagt dem Empfängerprogramm, dass die Datei auf dem PC geschlossen werden kann. Danach machen wir das Tor zum Kanal zu, sock.close(). Das ist in dieser Anwendung wichtig, weil sich die Dateiübertragung und der NTP-Zugriff das nic-Interface teilen müssen. Auf ein und denselben Parkplatz kann ja auch immer nur ein Auto gestellt werden.
def sendList(v,datei):
gc.collect()
sock=setSocket()
d.writeAt("SENDE LISTE",0,4)
print(len(v), "Werte")
s0=getS0()
S0=";"+str(s0)
sock.sendto((datei+"\r").encode(),receiver)
for n in range(len(v)):
gc.collect()
line=(str(n)+";"+str(v[n])+S0+"\r").encode()
sock.sendto(line,receiver)
sock.sendto("ende",receiver)
sock.close()
d.writeAt("DONE ",0,5)
sleep(3)
d.clearFT(0,4)
Und so wird die RTC synchronisiert. Wir versuchen mit ntp.time() einen Zugriff auf den Zeitserver, der die Weltzeit in Sekunden seit Epochenbeginn liefert. Diesen Wert müssen wir für die Ortszeit unter Berücksichtigung der Zeitzone korrigieren, timezone*3600. Localtime() macht aus den Sekunden ein Siebener-Tupel der Form:
(Jahr, Monat, Monatstag, Stunden, Minuten, Sekunden, Wochentag, Tag im Jahr)
Ds.DateTime() synchronisiert damit die RTC, indem die übernommenen Werte in die Register des DS3231-Chips geschrieben werden. Hat die Verbindung nicht geklappt, passiert gar nix, wir kriegen nur eine Nachricht im Terminal, oder im Display, wenn Sie eine programmieren mögen.
def synchronize():
try:
ds.DateTime(localtime(ntp.time()+timeZone*3600))
print("sychronized")
with open("lastSync.txt","w") as f:
f.write(name()[:-9]+"\n")
return ds.DateTime()
except:
print("Nicht synchronisiert")
Zu guter Letzt die Namensgebung für die Dateien. Das Ganze schaut ziemlich mystisch aus, ist aber bei genauerem Hingucken nur die Wiederholung einer kurzen Sequenz. Aber der Reihe nach. Wenn keine Datums-Zeit-Liste übergeben wird, holt sich name() selbst eine vom DS3231. Nun muss ein String daraus gebastelt werden, der stets dieselbe Länge haben muss, wegen der Sortierung der Liste von Dateinamen in files(). Um das zu erreichen, müssen einstellige Datums- oder Zeitwerte mit einer führenden 0 versehen werden. Genau das macht folgender Term gemäß der Vorschrift: wandle die Zahl in einen String um und setze eine "0" davor. Davon nimm ab dem vorletzten Zeichen (Index -2) alles bis zum Ende.
("0"+str(dt[1]))[-2:]
Aus dt[1] = 4 wird: "04", das vorletzte Zeichen ist 0, bis zum Ende also "04"
Aus dt[1] = 15 wird: "015", das vorletzte Zeichen ist 1, bis zum Ende also "15"
Zwischen die Daten und Zeitwerte werden "-"-Zeichen eingebaut, die Blöcke trennt jeweils ein "_". Abschließend wird noch "data.csv" angehängt.
def name(dt=None):
if dt is None:
dt=ds.DateTime()
fn=str(dt[0])+"-"+("0"+str(dt[1]))[-2:]+"-"+ \
("0"+str(dt[2]))[-2:]+"_"+("0"+str(dt[3]))[-2:]
fn=fn+"-"+("0"+str(dt[4]))[-2:]+"-"+ \
("0"+str(dt[5]))[-2:]+"_"
return fn+"data.csv"
Nun können wir auf die Früchte unserer Arbeit zugreifen. Wir ernten ein WLAN-Verbindungs-Objekt in sta. Die in der Funktion an das Display gelieferten Verbindungsdaten können 3 Sekunden lang gelesen werden. Messreihenspeicher deklarieren, Messreihenzähler auf 0, RTC synchronisieren und den Alarm-Timer für die Synchronisation stellen, jeden Tag zur elften Stunde.
sta=connect()
sleep(3)
s=[]
n=0
synchronize()
ds.Alarm2(11,0,0,DS3231.StundenAlarm)
Dann sind wir auch schon in der Hauptschleife. Speicher putzen, Messwert holen und Abweichung vom Basiswert berechnen. Der absolute Betrag davon entscheidet gleich darüber, ob eine Sequenz aufgezeichnet werden soll.
while 1:
gc.collect()
s1=getAdc(3)
trigger=s1-s0
deltaS=abs(trigger)
Das ist der Fall, wenn nämlich deltaS größer ist als das Grundrauschen dsc.
if deltaS > dsc :
bussy.on()
s=[]
over=TimeOut(1000)
while not over():
s.append(s1)
s1=getAdc(5)
sleep(0.001)
if getTouch(stop): break
bussy.off()
print("{}. Trigger: {}".format(n,deltaS))
d.writeAt("{}. Trig:{}".format(n,trigger),0,2)
d.clearFT(0,4)
save2sd(s)
n+=1
Die LED geht an, die Liste s wird geleert und der Timer für die Messzeit auf eine Sekunde gestellt.
Solange der Timer noch nicht abgelaufen ist, kommt der zuletzt eingeholte Wert vom ADXL335 als neues Element in die Liste. Ein neuer Wert wird geholt, kurzes Rasten und wenn die F-Taste nicht gedrückt ist, Ring frei zur nächsten Runde.
Die Schleife wird spätestens durch den Timer over() beendet. Wenn Sie Closure TimeOut() suchen sollten, die versteckt sich heute in der Klasse Buttons. Durch den Rundumschlag beim Import, kann ich darauf genauso zugreifen, als wäre sie als Funktion im Hauptprogramm deklariert.
from buttons import *
Wir erhalten Meldungen zur Messung, löschen dann die unteren beiden Zeilen im Display und schreiben die Liste in eine Datei, die in save2sd() als Dateinamen den aktuellen Zeitstempel erhält. Nun noch den Messreihenzähler erhöhen.
Wenn kein Erschütterungsmoment den Schleifendurchlauf unterbrochen hat, geht es mit gedrückter A-Taste ins Menü, oder mit der Flash-Taste zum Beenden des Programms.
In der Regel verfügt der PC bereits über eine Netzwerkverbindung, via Kabel oder WLAN, darum müssen wir uns nicht kümmern. Aber ein Tor zum Kanal müssen wir öffnen, einen - richtig, Socket.
import socket
myPort=9091
Dem ESP32 hatten wir anfangs gesagt, dass der PC unter der Hausnummer 9091 zu erreichen ist.
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind(('', myPort))
s.settimeout(0.1)
Die Socket-Instanziierung und die Einstellungen sind dieselben wie beim ESP32.
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind(('', myPort))
s.settimeout(0.1)
In der Hauptschleife startet sofort eine zweite while-Schleife, die auf den Eingang des Dateinamens wartet. Solange der Empfangspuffer nach Ablauf des Timeouts leer ist, wird eine Exception geworfen, die mit except abgefangen wird. Es muss nichts getan werden, als weiter zu warten, also - pass. Wurden Bytes empfangen, landen sie in rec, die Adresse des Senders in adr. Das bytes-Objekt wird in einen String decodiert und davon der Zeilenvorschub abgezwickt. Der Dateiname erscheint im Terminal, dann wird die Schleife mit break abgebrochen.
while 1:
while 1:
try:
rec,adr=s.recvfrom(150)
name=(rec.decode()).strip()
print(name)
break
except OSError:
pass
Mit dem Dateinamen wird jetzt eine Datei zum Schreiben geöffnet. Wieder gibt es eine Empfangsschleife, in der, abgesichert durch try, die Messwerte vom ESP32 eingelesen werden. Jede Zeile wird decodiert und im Terminal ausgegeben.
Wenn "ende" erkannt wird, kann die Datei geschlossen und die Schleife verlassen werden. Sonst landet die Zeile in der Datei.
Andere Fehler, außer dem durch recvfrom() bei leerem Empfangspuffer verursachten OSError, führen zu einem Programmabbruch.
Nach der Meldung "Waiting for data." Geht es zurück an den Anfang der Hauptschleife.
Natürlich können Sie nicht nur den ADXL335 als Sensor im Zusammenhang mit den dargestellten Programmen nutzen. Viele weitere Sensoren warten nur darauf, entdeckt und eingesetzt zu werden.
]]>Es wird eng mit dem Speichern von Dateien auf dem ESP32, vor allem dann, wenn man die Aufzeichnungsdauer auf einige Sekunden ausdehnt. Mit fünf Einzelmessungen liefert die Methode getAdc(5) immerhin 600 Messpunkte pro Sekunde. Das könnte man noch steigern, wenn man die Pause von einer Millisekunde in der Messschleife herausnimmt. Pro 100 Messpunkte muss man mit einer Speichergröße von ca. 1KB rechnen. Wir brauchen also mehr Speicherplatz für den Dauerbetrieb. Den bekommen wir mit dem Einsatz einer SD-Speicherkarte. Bei 8GB Kapazität bringen wir gut 250000 Dateien zu je 30KB unter. Das entspricht 250000 Trigger-Events zu je fünf Sekunden Aufzeichnungsdauer. Also flugs ein SD-Karten-Modul besorgt und dazu eine Micro-SD-Karte.
Für diese Aufzeichnungsdauer ist die bisherige grafische Anzeige zu träge. Außerdem fehlt die Möglichkeit im Graphen auch „zurückzurudern“. Für die ersten Versuche in diese Richtung hatte ich wegen der benötigten Tasten ein LCD-Keypad im Einsatz. Das hat aber nicht meine Erwartungen an den Bedienungskomfort erfüllt, weswegen ich schließlich zu einem Joystickmodul gegriffen habe. Das besitzt auch sechs Tasten und mit dem Joystick ist es ein Vergnügen, mit verschiedenen Geschwindigkeiten durch den Graphen zu scrollen und durch die Dateiliste zu blättern. Wie ich das alles umgesetzt habe, erfahren Sie in diesem Beitrag aus der Reihe
heute
Die Teileliste umfasst auch die Hardware von der vorangegangenen Folge. Ergänzt habe ich den SPI-Card-Reader und das Joystick-Shield. Letzteres ist eigentlich für den Arduino gedacht, funktioniert aber auch prächtig mit dem ESP32. Der kann nun gut seine Trümpfe ausspielen, denn selbst nach dem Verbinden der sechs Tasten auf dem Shield mit dem Controller bleiben immer noch freie GPIOs übrig.
Vier davon brauchen wir aber schon einmal für den SPI-Bus zum Card-Reader.
1 |
oder ESP32 NodeMCU Module WLAN WiFi Development Board oder NodeMCU-ESP-32S-Kit |
1 |
GY-61 ADXL335 Beschleunigungssensor 3-Axis Neigungswinkel Modul |
1 |
|
1 |
|
1 |
Mehrgang rotary Potentiometer mit Schutzwiderstand 3590S 10K Ohm |
1 |
|
1 |
SPI Reader Micro Speicher SD TF Karte Memory Card Shield Modul |
1 |
Micro-SD-Card, 4GB – 32GB |
1 |
LED |
1 |
Widerstand 270 Ohm |
diverse |
Jumperkabel |
|
Digital-Voltmeter (DVM) für die Kalibrierung |
Die Anordnung der Teile zeigt Abbildung 1. Der Card-Reader steckt links oben.
Abbildung 1: Seismograph mit SD-Karte und Joystickmodul
Fürs Flashen und die Programmierung des ESP32:
Thonny oder
ssd1306.py Hardwaretreiber für das OLED-Display
oled.py API für OLED-Displays
sdcard.py Treiber für das SD-Reader-Modul
buttons.py API für den Betrieb von Tasten
earthquake_sd.py Betriebsprogramm
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 05.02.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Abbildung 2: Seismometer - Erweiterte Schaltung
Ursprünglich hatte ich eine der Tasten an GPIO12 angeschlossen. Alles lief problemlos - bis zum nächsten Reset. Der ESP32 konnte keine Verbindung mit Thonny herstellen. Es stellte sich heraus, dass der ESP32, wie auch der ESP8266, gewisse Anschlüsse beim Booten auf einem bestimmten Pegel haben möchte. Folgende GPIOs sind davon betroffen.
Durch den Pullup-Widerstand an der Taste A konnte der LOW-Zustand nicht erreicht werden, die Folge war der missglückte Start des Boards. Nach dem Umzug auf den Anschluss GPIO27 war das Problem behoben.
Mit dem SPI-Bus haben wir uns neben dem I2C-Bus eine weitere serielle Übertragungstechnik ins Boot geholt. Beide Systeme arbeiten bidirektional, und bei beiden Systemen ist der Controller der Chef. Beim I2C-Bus darf aber immer nur ein Partner zuhören, während der andere sendet. Beim SPI-Bus geht beides zur gleichen Zeit. Das liegt an der Anzahl von Leitungen, zwei sind es beim I2C-Bus, der SPI-Bus braucht deren vier. Beiden Systemen gemeinsam ist eine Taktleitung SCL/SCK, die der Controller als Chef (Master) bedient. Beim I2C-Bus wandern die Anfragen vom Master über die SDA-Leitung zur Peripherie (Slave) und von dort kommt die Antwort auf derselben Leitung zurück zum Master. Das geht eben nicht gleichzeitig.
Beim SPI-Bus gibt es eine Sendeleitung (MOSI = Master Out Slave In) und eine Empfangsleitung (MISO = Master In Slave Out). Während der Master den nächsten Auftrag an den Slave sendet, kann dieser im Prinzip mit derselben Taktfolge auf den vorigen Auftrag antworten. Die Taktfrequenz auf SCK kann wesentlich höher sein als beim I2C-Bus, wird aber letztlich durch den oder die Slaves begrenzt.
Abbildung 3: Bussysteme im Vergleich
Die Auswahl des Peripheriebausteins erfolgt beim I2C-Bus über eine sogenannte Hardwareadresse. Die ist beim SPI-Bus nicht erforderlich, der Baustein wird über eine spezielle Auswahlleitung (CS = Chip Select) angesprochen.
Für beide Bussysteme gibt es in MicroPython Klassen, I2C oder SoftI2C und SPI, die im Modul machine wohnen und Funktionen für die Abwicklung des Datenverkehrs bieten.
Direkteingaben im Terminal von Thonny (REPL) sind im Text fett formatiert und am Prompt >>> zu erkennen. Antworten vom ESP32 sind kursiv gesetzt.
Wir testen das einmal. Mit dem Logic Analyzer zeichne ich die Signale auf den Leitungen SCK, MOSI und MISO auf. Der Card-Reader hat vorher noch keinen auswertbaren Befehl erhalten, die MISO-Leitung wird als HIGH eingelesen. Ich sende 0xA5 und lese gleichzeitig ein Byte über MISO ein, 0xFF. Befremdend mag erscheinen, dass mit einem read-Befehl gesendet wird, aber das liegt eben in der Natur des SPI-Transfers. Natürlich gibt es in der Klasse SPI auch einen reinen write-Befehl. Das erste Argument bei read(1,0xA5) bezeichnet die Anzahl einzulesender Bytes, das zweite Argument ist das zu sendende Byte. Daneben gibt es auch Funktionen, die bytes-Objekte oder bytearrays senden und empfangen. Eine Übersicht über die Objekte in der Klasse SPI erhalten Sie so:
>>> dir(SPI)
['__class__', '__name__', 'read', 'readinto', 'write', '__bases__', '__dict__', 'LSB', 'MSB', 'deinit', 'init', 'write_readinto']
Aber jetzt zum Test. Die Taktfrequenz von 100kHz wurde durch die Klasse SDCard vorgegeben.
>>> spi.read(1, 0xA5)
b'\xff'
Abbildung 4: Senden 0xA5 - empfangen 0xFF
Die Parameter der SPI-Schnittstelle ruft folgender Befehl ab.
>>> spi
SPI(id=1, baudrate=100000, polarity=0, phase=0, bits=8, firstbit=0, sck=18, mosi=23, miso=19)
Das Protokoll ist in unserem Fall so eingestellt, dass der Master das Datenbit vor dem ersten Takt, bei fallender Flanke auf MOSI bereitstellt und der Slave dieses Bit mit der steigenden Flanke des Takts übernimmt. Der Controller sampelt die MISO-Leitung mit fallender Taktflanke.
Der Logic Analyzer sagte mir auch, dass bei reinen Lesebefehlen lauter 0xFF-Bytes gesendet werden. Das bedeutet, dass die MOSI-Leitung zum Beispiel beim Einlesen von der Speicherkarte permanent auf HIGH liegt. Wenden wir uns jetzt dem Programmzuwachs und den notwendigen Änderungen zu.
Zur Konversation auf dem SPI-Bus brauchen wir die Klasse SPI aus machine und das externe Modul sdcard. Für die Behandlung von Tastenaktionen hole ich mir ferner das Modul buttons. Es erzeugt Tasten-Objekte und stellt eine Reihe von Funktionen zum Einlesen des Tastenzustands zur Verfügung. Der Stern holt alle Bezeichner aus buttons.py in den globalen Namensraum und erspart mir das Präfix Buttons bei jedem Aufruf einer Funktion. Das funktioniert genauso wie das selektive from time import sleep, ticks_ms.
from machine import Pin, ADC, SoftI2C, freq, SPI
from time import sleep,ticks_ms
import os, sys, sdcard
from oled import OLED
from esp32 import NVS
from buttons import *
freq(240000000)
Hier kommen auch gleich die sechs Tasten-Objekte, die alle LOW-aktiv sind. Ich erhaltet eine 1 bei gedrückter Taste, wenn ich invert auf True setze. Die 1 vereinfacht die Abfrage, weil eine 1 als True gewertet wird.
if getTouch(keyA):
Liest sich einfach wie "wenn die Taste A gedrückt ist…". Das ist intuitiver als
if keyA.value() == 0:
stop=Buttons(4,invert=True,pull=True,name="stop") # F stop
ende=Buttons(0,invert=True,pull=True,name="ende") # Flash
confirm=Buttons(16,invert=True,pull=True,name="confirm") # E
keyD=Buttons(17,invert=True,pull=True,name="Taste D") # Datei
keyB=Buttons(13,invert=True,pull=True,name="Taste B") # grafik
keyA=Buttons(27,invert=True,pull=True,name="Taste A") # menu
keyC=Buttons(14,invert=True,pull=True,name="Taste C") # menu exit
Als SPI-Bus-Interface verwenden wir die Einheit 1 mit den Pins
spi=SPI(1,baudrate=100000,sck=Pin(18),mosi=Pin(23),\
miso=Pin(19),polarity=0,phase=0)
Die Einrichtung der SD-Karte erfolgt in zwei Schritten. Als Erstes erzeugen wir eine SDCard-Instanz.
try:
sd = sdcard.SDCard(spi, Pin(4))
except:
print("SD-Card init failed")
while 1:
led.value(0)
sleep(0.3)
led.value(1)
sleep(0.5)
if getTouch(ende):
led.value(0)
print("Cancelled by flash key")
print("restart with RST key")
d.writeAt("SD init failed",0,1)
sys.exit()
Kann die Speicherkarte aus irgend einem Grund nicht angesprochen werden, das prüft der Konstruktor über die Routine SDCard.init_spi(), dann bekommen wir eine Fehlermeldung im Terminal und eine blinkende LED. Die Programmsequenz habe ich als Baustein aus meinem Fundus übernommen, da heißt der LED-Ausgang led. Indem ich für den Ausgang bussy das Alias led einführe, kann ich dieselbe LED ohne Änderungen am Text des Bausteins, sowie am Programmtext einmal als bussy und ein andermal als led verwenden.
bussy=Pin(2,Pin.OUT,value=0)
led=bussy
Hat die Initialisierung der Karte geklappt, dann muss der Speicher als Ordner in den Verzeichnisbaum des ESP32 eingehängt, gemountet, werden. Wie bei der Instanziierung des Karten-Objekts, sichern wir den Vorgang mit try – except ab.
try:
os.mount(sd, '/sd')
print("SD-Card is mounted on /sd")
d.writeAt("SD IS MOUNTED",0,1)
except OSError as e:
print(e)
print("SD-Card previously mounted")
Die Karte ist jetzt über das Verzeichnis /sd ansprechbar. Wir müssen nur den Verzeichnisbaum des ESP32 auffrischen, dann können wir das Verzeichnis in Thonny öffnen.
Abbildung 5: Das Verzeichnis sd in die Anzeige holen
Abbildung 6: Die Karte ist gemountet
Mit Doppelklick auf sd sehen wir die enthaltenen Dateien, oder besser, wir sehen sie nicht, weil wir ja in sd noch nichts abgelegt haben.
Ein Großteil der nächsten 50 Programmzeilen wurden 1:1 aus dem Listing der Vorgängerversion dieser Ausgabe übernommen. Dort sind sie auch dokumentiert. Die neuen Bereiche sind fett formatiert. Hier geht es um die Deklaration und Initialisierung der analogen Anschlüsse für den Joystick. Dabei hat sich herausgestellt, dass eine Änderung der Auflösung an den Analogeingängen nur bei Pin 36 möglich ist. Die anderen Analoganschlüsse arbeiten, auch wenn zum Beispiel WIDTH_9BIT, wie im Listing, eingestellt wird, trotzdem mit 12 Bit. Wenn der Knüppel auf dem rechten Anschlag liegt, erhalten wir statt 511 4095:
>>> joyX.read()
4095
sleep(3)
nvs=NVS("Quake")
adcPinNumber=36
adc=ADC(Pin(adcPinNumber))
joyXnumber=39
joyYnumber=34
joyX=ADC(Pin(joyXnumber))
joyY=ADC(Pin(joyYnumber))
joyX.width(ADC.WIDTH_9BIT)
joyX.atten(ADC.ATTN_11DB) # 150 - 2450 mV
joyY.width(ADC.WIDTH_9BIT)
joyY.atten(ADC.ATTN_11DB) # 150 - 2450 mV
adc.atten(ADC.ATTN_11DB) # 150 - 2450 mV
adc.width(ADC.WIDTH_12BIT)
# 0...4095; LSB = 3149mV/4095cnt=0,769mV/cnt
# @313mV/g (313mV/g)/0,769mV/cnt = 408cnt/g
# LSBg = 1/408cnt/g = 2,45mg/cnt
lsbC=3149/4095 # mV / cnt
lsbG=lsbC/313 # g / mV
s0=0
su=4095
so=0
n=1000
m=[]
for i in range(n):
s=adc.read()
m.append(s)
s0+=s
su=min(su,s)
so=max(so,s)
s0=int(s0/n)
nvs.set_i32("s0",s0)
nvs.commit()
dsu=int(s0-su+1)
dso=int(so-s0+1)
dsc=max(dsu,dso)*2
ds = dsc*lsbC*lsbG
print ("s0= {}; su={}; so= {}; dsu={}; dso= {}".format(s0,su,so,dsu,dso))
print ("Rauschen: {} cnts= {:0.2f} g".format(dsc,ds))
sx,sy=0,0 # Nullstellung des Joysticks bestimmen und merken
for i in range(50):
sx+=joyX.read()
sy+=joyY.read()
nvs.set_i32("joyx",int(sx/50))
nvs.set_i32("joyy",int(sy/50))
nvs.commit()
Die Werte, die sich aus der Ruhestellung des Joysticks ergeben, legen wir, wie den s0-Wert, auch im nichtflüchtigen Speicher ab.
getAdc() und getS0() wurden ebenfalls unverändert übernommen.
def getAdc(n):
s=0
for i in range(n):
s+=adc.read()
return int(s/n)
def getS0():
return nvs.get_i32("s0")
Ganz neu ist die Joystickabfrage. Der Joystick enthält zwei Potentiometer, deren Schleifer durch Neigen des Knüppels verstellt wird. Die Enden der Potis liegen an +3,3V und GND, sodass wir Spannungen aus diesem Bereich an den Kontakten x und y des Joystick-Shields erhalten. Den x-Anschluss habe ich an GPIO39 geführt, y liegt an GPIO34.
Die Funktion für die Joystickabfrage arbeitet, ähnlich wie die Abfrage des Accelerometers, zunächst mit Mittelwertbildung. Allerdings wird nicht der Absolutwert der ADC-Counts weitergegeben, sondern der Relativwert zur Mittelstellung, reduziert mit dem Teiler 100. Es ergeben sich somit Werte zwischen -20 und +20.
def joystick(n):
sx,sy=0,0
for i in range(n):
sx+=joyX.read()
sy+=joyY.read()
x,y=int(sx/n),int(sy/n)
x=int((x-joyX0)/100)
y=int((y-joyY0)/100)
return x,y
Geändert hat sich die Routine zum Einlesen von Dateien, aber nur an der Stelle, wo es um den Aufbau des Dateinamens geht. Der muss natürlich jetzt das Präfix /sd/ erhalten.
def readData(n):
global s
s=[]
name="/sd/data"+str(n)+".csv"
try:
with open(name,"r") as f:
for line in f:
zeile=line.strip()
if zeile.find(";") != -1:
nr,s1=zeile.split(";")
val=int(s1)
s.append(val)
except OSError as e:
print("Datei nicht gefunden",e)
d.writeAt("data{} not found".format(n),0,5)
sleep(3)
d.clearAll()
Einen größeren Umbau hat die Funktion grafik() erhalten. Lief vorher der Graph einer Aufzeichnung einfach durch, so wird der Viewport jetzt mittels Joystick quasi über dem Datensatz eines Events hin- und hergeschoben. Betrachtet wird stets die Liste s, in welche der Inhalt einer Datei vorher eingelesen werden muss.
def grafik(v):
d.clearAll()
ml=dheight//2+1
laenge=len(v)
print(laenge)
so=max(v)
su=min(v)
s0=getS0()
ds=max(so-s0,s0-su)
yFaktor=(dheight/(ds*2))
i=0
while 1:
x,y=joystick(5)
i=min(i+x,laenge - 64)
i=max(1,i)
d.clearAll(False)
d.hline(0,ml,127,1)
d.writeAt(str(i),16-len(str(i)),0,False)
x1=0
y1=ml-int((s[i]-s0)*yFaktor)
bis=63 if i+64 < laenge else laenge - i
for j in range(i,i+bis):
x2=(j-i+1)*2
y2=ml-int((v[j]-s0)*yFaktor)
d.line(x1,y1,x2,y2,1)
x1=x2
y1=y2
d.show()
if getTouch(stop): break
Bis zur while-Schleife, die die äußere for-Schleife der Vorgängerversion ersetzt, blieben die Zeilen unverändert.
Wir starten mit dem Einlesen der Joystickposition durch fünf Einzelmessungen. Der Index i in die Liste s darf höchstens bis zur 64. Position vor dem Listenende laufen. Dass die Summe aus der gegenwärtigen Position und der Joystickstellung x diese Marke nicht überschreiten kann, garantiert die Funktion min(). max() stellt sicher, dass die 1 nicht unterschritten wird, wenn ein negativer x-Wert den Index verringert.
Wir löschen die Anzeige verdeckt (False) und zeichnen die Mittenlinie. Die Funktion hline() erbt OLED über SSD1306_I2C von der Klasse framebuf.FrameBuffer. Die Funktionen von FrameBuffer schreiben fast alle nur in den Puffer. Die Funktion show(), die durch die Vererbung in den Namensraum von OLED geholt wurde, sendet die Pufferdaten dann erst ans Display. Auch den Index i schreiben wir rechtsbündig und verdeckt in die rechte obere Ecke des Displays.
Der ADC-Wert an der Position i wird in eine y-Koordinate umgerechnet, Differenz zum Ruhewert S0 mal y-Skalenfaktor. Den ganzzahligen Anteil davon subtrahieren wir vom y-Wert der Mittenlinie. Subtraktion deshalb, weil die y-Achse des Displays von oben nach unten zeigt. Positive Werte kommen damit in die obere Hälfte, negative in die untere.
Damit der Index beim Zeichnen nicht über das Ende der Liste s hinausrennt, muss der Summand bis, mit dem wir den Bereich der x-Werte festlegen, ebenfalls begrenzt werden. Wir berechnen die Koordinaten des nächsten Pixels, ziehen eine Linie von der ersten zur zweiten Position und übernehmen die Koordinaten des zweiten Punkts in den ersten.
Nach dem Verlassen der for-Schleife ist der Puffer gefüllt, wir schieben die Daten mit show() ins Display.
Durch die Deklaration der Funktion save2sd() wird die Sicherung der Daten aus der Hauptschleife ausgelagert. Das erhöht die Lesbarkeit der Hauptschleifenstruktur und verringert die Durchlaufzeit. In v übergeben wir die Liste s (oder einen Teil davon) und in n die Nummer der zu bildenden Datei.
def save2sd(v,n):
aw=len(v)
name="/sd/data"+str(n)+".csv"
print("***** Speichere {} Werte auf SDCard *****".\
format(aw))
d.writeAt("SAVING data{}".format(n),0,4)
with open(name,"w") as f:
for i in range(aw):
f.write("{};{}\n".format(i,v[i]))
print("============= FERTIG ================")
d.writeAt("DONE!",0,5)
sleep(3)
d.clearFT(0,4)
Die Länge der Liste wird bestimmt und der Dateiname zusammengebaut. Im Terminal und Display werden wir über den Stand der Dinge informiert. Mit with öffnen wir die Datei zum Schreiben. Die for-Schleife schickt die Nummer-Wert-Paare auf die SD-Karte. Vor dem Verlassen der Funktion löschen wir den Bereich der Mitteilungen im Display, Zeile 4, Spalte 0 bis Displayende Zeile 5, Spalte 15.
Die sechs Tasten erlauben zwar bereits die Auslösung verschiedener Aktionen, aber mit einer Ausdehnung auf weitere Features würden wir sicher an Grenzen stoßen. Abhilfe schafft ein Menü. Dadurch können wir den Tasten mehrere Funktionen zuweisen, programmtechnisch mit einem OOP-Begriff gesprochen, wir können sie überladen.
def Menu():
while 1:
d.clearAll(False)
d.writeAt("MENU",0,0,False)
d.writeAt("D Dateien",0,1,False)
d.writeAt("C EXIT",0,5)
t=waitForAnyKey((keyD,keyC),5000)
if t==0:
if waitForRelease(keyD,2):
files()
elif t==1:
d.clearAll()
return
In der while-Schleife geben wir Überschrift, Tastenbezeichnung und Bedeutung aus. Die Funktion waitForAnyKey() aus dem Modul buttons wartet 5 Sekunden lang auf die Betätigung einer der Tasten, deren Instanzen als Elemente des Tupels an den Positionsparameter tasten übergeben wurden. Wird 5000ms lang keine Taste gedrückt, kommt der Wert None zurück, sonst die Positionsnummer des Tasten-Objekts im Tupel.
Wir prüfen auf diese Nummern. Weil im Fall der Taste D die Funktion files() aufgerufen wird und dort dieselbe Taste eine weitere Aktion auslöst, warten wir kurz darauf, dass die Taste losgelassen wird. Kommt True zurück, wird files() aufgerufen, sonst passiert gar nix.
Mit C steigen wir aus dem Menü aus. Das Menü kann leicht erweitert werden, indem neue Menüpunkte an die Reihe der Displayausgaben angehängt werden und die if-elif-Struktur durch weitere Vergleiche ergänzt wird.
Die Funktion files() behandelt Aktionen rund um das Thema Dateien. Nach dem Löschen des Displays holen wir die Liste der in /sd vorhandenen Dateien, bestimmen die Anzahl und sortieren die Liste. Der Laufindex i wird mit 0 initiiert.
In der while-Schleife geben wir den ersten Dateinamen aus und fragen den Joystick ab. In y-Richtung bewegt, blättern wir uns durch die Dateinamenliste nach oben aufsteigend nach unten absteigend. Mit min() und max() erzwingen wir nur gültige Indexwerte.
Die Taste D schaufelt den Inhalt der aktuell angezeigten Datei in die Liste s, indem die Funktion readData() aufgerufen wird.
Mit der Taste F löschen wir die Anzeige und kehren zum aufrufenden Programm zurück.
Die Taste E dient zum Löschen der aktuell angezeigten Datei. remove() erledigt das auf der Speicherkarte, die Anweisung del entfernt den Namen aus der Liste dateien. Wir merken uns schon mal den Namen der Datei. Die Anzahl der Namen und die Anzeige müssen nach dem Löschen angepasst werden.
Zur Anzeige der Grafik kommen wir mit der Taste B.
def files():
d.clearAll()
dateien=os.listdir("/sd")[1:]
n=len(dateien)
dateien.sort()
i=0
while 1:
d.writeAt("{} ".format(dateien[i]),0,0)
x,y=joystick(5)
if y>0:
i=min(i+1,n-1)
elif y<0:
i=max(i-1,0)
if getTouch(keyD): # Datei laden
num=dateien[i][4:dateien[i].index(".")]
d.writeAt("load data{} ".format(num),0,4)
readData(num)
d.writeAt("got data{} ".format(num),0,5)
print("eingelesen:",dateien[i])
sleep(3)
d.clearFT(0,4)
if getTouch(stop): # F files verlassen
d.clearAll()
return
if getTouch(confirm): # E Datei loeschen
num=dateien[i][4:dateien[i].index(".")]
os.remove("/sd/"+dateien[i])
del dateien[i]
n=len(dateien)
d.writeAt("data{} killed ".format(num),0,5)
sleep(3)
d.clearFT(0,5)
if getTouch(keyB):
grafik(s)
d.clearAll(False)
sleep(0.2)
joyX0,joyY0=nvs.get_i32("joyx"), nvs.get_i32("joyy")
Wir holen die Nullstellung des Joysticks aus dem nichtflüchtigen Speicher. Es folgen Informationen zum Basiswert und dem Grundrauschen auf dem Display. Mit Taste A wird das Menü aufgerufen. Messwertliste leeren und Dateinummerierung auf 0.
d.clearAll()
d.writeAt("s0 ={}".format(s0),0,0)
d.writeAt("dsc={}".format(dsc),0,1)
d.writeAt("A >> Menu".format(dsc),0,5)
s=[]
n=0
In der Hauptschleife kümmern wir uns sogleich um einen aktuellen ADC-Wert. Die Differenz mit S0 liefert uns die Höhe des eventuellen Trigger-Pegels. Der absolute Wert davon ist die Abweichung ds, die wir mit dem Grundrauschen dsc vergleichen.
Übersteigt ds das Grundrauschen, liegt eine Bewegung des Untergrunds vor. Die LED geht an, und eine leere Messwertliste wird vorgelegt. Wir stellen den Timer für die Messdauer. In der while-Schleife wird die aktuelle Messung an die Liste angehängt, ein neuer Wert wird geholt, kurze Pause von einer Millisekunde. Ist die Stoptaste F nicht gedrückt, geht es in die nächste Runde, sonst wird der Schleifendurchlauf mit break beendet. Das Ende der Aufzeichnung ist auch dann erreicht, wenn der Timer over() abgelaufen ist. Die LED geht aus, und wir erfahren den Wert des Triggers. Der untere Bereich der Anzeige wird geputzt, die Liste s gespeichert und die Dateinummer erhöht - fertig.
Mit der Taste A kommen wir ins Menü, die Flash-Taste des ESP32 beendet den Programmlauf, nachdem die SD-Karte ausgehängt wurde
while 1:
s1=getAdc(3)
trigger=s1-s0
ds=abs(trigger)
if ds > dsc :
bussy.on()
s=[]
over=TimeOut(1000)
while not over():
s.append(s1)
s1=getAdc(5)
sleep(0.001)
if getTouch(stop): break
bussy.off()
print("{}. Trigger: {}".format(n,ds))
d.writeAt("{}. Trig:{}".format(n,trigger),0,2)
d.clearFT(0,4)
save2sd(s,n)
n+=1
if getTouch(keyA):
Menu()
if getTouch(ende): # Flash
os.umount("/sd")
d.clearAll()
d.writeAt("PROGRAM",0,0)
d.writeAt("CANCELED",0,1)
sys.exit()
Um die Speicherkarte aus dem Slot zu entfernen, sollten Sie das Programm mit der Flashtaste beenden, damit sie automatisch aus dem Dateisystem ausgehängt wird, umount() übernimmt das. Alternativ wären zwei weitere Menüpunkte eine interessante Möglichkeit, Taste A - Karte aushängen (umount), Taste E - Karte einhängen (mount). Denken Sie aber auch daran, versehentliches Auslösen der Aktionen abzusichern, Stichwort waitForRelease(), wenn Sie den Vorschlag in die Tat umsetzen.
Immerhin haben Sie mit der SD-Karte eine passable Möglichkeit, die Daten auf den PC zu holen, um sie mit EXCEL auswerten zu können. Wie das geht habe ich ja schon im vorangegangenen Beitrag beschrieben. In der nächsten Folge wird sich ein RTC-Modul (DS3231) dazugesellen, und wir werden die Dateien per WLAN an beliebige Empfänger übertragen. Damit im Zusammenhang steht eines von zwei Problemen, deren Lösung ich Ihnen dann auch verraten werde.
Bleiben Sie dran, bis dann!
]]>Um Erschütterungen, oder besser Schwingungen, des Untergrundes zu erfassen, gibt es zwei Arten von Sensoren. Die einfachere Art ist ein Rüttelkontakt. Das kann eine kleine Kugel in einem Röhrchen sein, die durch einen Stoß eine Verbindung zwischen zwei Kontakten herstellt oder öffnet. Eine andere Bauart verwendet eine kleine Blattfeder an deren Ende eine Schwungmasse sitzt. Erfährt das Bauteil einen Stoß, dann gerät die Feder in Schwingung und schließt den Kontakt zu einer zweiten Feder oder dem Gehäuse. Solche Rüttel-Sensoren können nur "an" und "aus", sie kennen keine Zwischentöne und sagen nichts über die Größenordnung der Stöße aus.
Die andere Art von Sensor verwende ich hier. Es ist ein Beschleunigungssensor oder Accelerometer. Dessen Funktion ist vergleichbar mit dem eben beschriebenen Federkontakt. Nur bestehen hier die Federn aus hauchdünnen Piezo-Plättchen. Werden die durch die Schwerkraft oder durch Erschütterungen verbogen, dann geben sie eine Spannung ab, die zur Verbiegungsamplitude, also auch zur wirkenden Kraft, proportional ist. Damit kann die Heftigkeit eines Stoßes festgestellt werden.
Abbildung 1: XL335B - 3-Achsen-Accelerometer mit analogen Ausgängen
Aber auch in diesem Fall gibt es zwei Typen von Sensoren, die sich vor allem in der Art der Datenübertragung zum Controller unterscheiden. Die einen werden über den I2C-Bus angesteuert und abgefragt, die anderen geben die Spannung der Piezoplättchen, natürlich über einen integrierten Verstärker, an Analogausgängen ab. So ein Bauteil ist der ADXL335, der hier in dem BOB (Break Out Board) GY-61 zusammen mit einem Spannungsregler verbaut ist. Ich werde im Folgenden beschreiben, wie man aus dem GY-61, einem OLED-Display und einem ESP32 ein Seismometer, oder einen Seismographen bauen kann. Außerdem zeige ich eine weitere Möglichkeit, wie man dauerhaft Daten im Flash des ESP32 ablegen und abrufen kann und es gibt eine Darstellung der Funktionsweise eines Accelerometers. Folgen Sie mir auf eine neue Tour zum Thema
heute
1 |
oder ESP32 NodeMCU Module WLAN WiFi Development Board oder NodeMCU-ESP-32S-Kit |
1 |
GY-61 ADXL335 Beschleunigungssensor 3-Axis Neigungswinkel Modul |
1 |
|
1 |
|
1 |
Mehrgang rotary Potentiometer mit Schutzwiderstand 3590S 10K Ohm |
1 |
LED |
1 |
Widerstand 270 Ohm |
diverse |
Jumperkabel |
|
Digital-Voltmeter (DVM) für die Kalibrierung |
Der Aufbau gestaltet sich sehr einfach, das OLED-Display wird an den I2C-Bus angeschlossen. Den Chefposten übernimmt ein ESP32. Für dessen Einsatz bedarf es zweier Breadboards, die mit einer Stromschiene in der Mitte verbunden werden, damit man mit den Abständen der Pinreihen hinkommt und am Rand auch noch Kabel gesteckt werden können.
Abbildung 2: Erdbeben-Sensor - Aufbau
Für den ESP32 habe ich mich deshalb entschieden, weil der mit einem 12-Bit ADC aufwarten kann und darüber hinaus eine Auswahl des Spannungsbereichs bietet. Für den weiteren Ausbau des Projekts kommt uns die reichliche Auswahl an GPIO-Pins zu Gute. Grundsätzlich wäre für den Umfang dieses Projekts auch ein ESP8266 geeignet. Das Display hat eine Größe von 128x64 Pixeln und dient unter anderem zur grafischen Darstellung der Messwerte.
Fürs Flashen und die Programmierung des ESP32:
Thonny oder
ssd1306.py Hardwaretreiber für das OLED-Display
oled.py API für OLED-Displays
earthquake.py Betriebsprogramm
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 05.02.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Zur Erfassung betrachte ich erst einmal nur die vertikale Richtung. Beim Sensor ist das die z-Achse. Sie zeigt nach oben, wenn man den Sensor flach auf den Tisch legt. Das entsprechende Piezoelement wird dann durch die Gewichtskraft Fg auf die Masse nach unten verbogen. Drehe ich das BOB in die vertikale Ausrichtung, senkrecht zur Tischfläche, findet keine Biegung statt, weil die Schwerkraft in Richtung des Plättchens wirkt. Drehe ich das BOB um weitere 90°, so dass es auf dem Kopf steht, dann wird das Plättchen in die Gegenrichtung gebogen. Der Messverstärker stellt für die drei Fälle am Ausgang Spannungen ein, die der Ungleichungskette in Abbildung 3 entsprechen.
Abbildung 3: Arbeitsprinzip eines Piezoelements im ADXL335
Bleiben wir bei der ersten Ausrichtung parallel zur Tischfläche. Die Ausgangsspannung U1 ist für unser Vorhaben die Bezugsgröße. Der ADC des ESP32 liefert in diesem Fall einen Wert von um die 2368 counts. Klopfe ich jetzt mit dem Finger auf die Tischplatte, dann versetze ich sie in Schwingung, sie wird sich schnell hintereinander etwas heben und senken. Sie können sich das veranschaulichen, wenn Sie das eine Ende eines Lineals mit der einen Hand am Tisch festklemmen und mit der anderen Hand das überstehende Ende kurz antippen. Das ist übrigens auch gleich ein Modell für die Funktionsweise der Piezoelemente.
Jetzt kommt eine weitere Sache ins Spiel, die Trägheit der Masse. Das ist die Eigenschaft von Körpern, sich einer Bewegungszustandsänderung zu widersetzen. Daraus resultieren Kräfte Ft, deren Richtung der Richtung der Bewegung genau entgegengesetzt ist.
Abbildung 4: Kräftebilanz und Spannungen beim Schwingen
Hebt die schwingende Tischplatte das BOB an, bewirkt die Trägheit der Masse m am Piezoarm, die an ihrem Ort bleiben möchte, eine stärkere Biegung des Piezoelements nach unten, es entsteht am Ausgang des Messverstärkers eine höhere Spannung Uanheben. Im Gegenzug verringert sich aus dem gleichen Grund die Biegung des Elements, wenn das BOB mit der Tischplatte nach unten geht. Die Spannung am Ausgang sinkt.
Die Schwingungen erfolgen um die Ruhelage und bringen am ADC eine Differenz der Zählerpunkte (counts) von 18 (leichtes Klopfen) bis 242 (Faustschlag). Letzteres entspricht, wie wir später noch sehen werden, ca. 0,5g. Die Erdbeschleunigung g = 9,81m/s² bewirkt über die Newtonsche Formel Fg = m • g die Verbiegung des Piezoelements. Die Massenträgheit führt durch eine Negativbeschleunigung a, bezüglich der Bewegungsrichtung, zur Trägheitskraft Ft = m • a, welche die Gewichtskraft Fg überlagert. Beim Anheben werden die Kräfte addiert, beim Absenken verringert sich die Gewichtskraft um den Betrag von Ft.
Abbildung 5: Seismometer - Schaltung
Die Beschaltung der drei Baugruppen ist denkbar einfach. Die Spannungsversorgung sollte für einen Langzeitbetrieb aus einem 5V-Steckernetzteil erfolgen. Während der Entwicklung versorgt der PC die Schaltung über den USB-Bus.
Man kann die Aufgaben des Programms in drei Bereiche aufteilen: Erfassen, Darstellen und dem PC die Daten zur weiteren Auswertung bereitstellen.
Die Erfassung geschieht in der Hauptschleife und startet automatisch durch das Auftreten einer Bodenschwingung, die mindestens den doppelten Pegel des Grundrauschens erreicht. Die Messdauer beträgt dann eine halbe Sekunde. Während dieser Zeit wird eine Liste aufgebaut, deren Werte nach Ablauf des Timers in eine Datei im Flash des ESP32 geschrieben werden. Die Benennung der Dateien geschieht automatisch über einen Zähler. Aber schauen wir uns das einfach mal der Reihe nach an.
from machine import Pin, ADC, SoftI2C
from time import sleep,ticks_ms
from sys import exit
from oled import OLED
from esp32 import NVS
Wir brauchen Pin-Objekte, den ADC und die I2C-Schnittstelle, ferner sleep und ticks_ms für Verzögerungen. Mit exit() bauen wir einen geordneten Ausstieg aus dem Programm. Die Klasse OLED liefert die API für das Display. NVS ist das Akronym für Non Volatile Storage = nicht flüchtige Speicherung. Gemeint ist eine Speichermöglichkeit für 32-Bit Integerwerte und sogenannte Blobs, das sind bytes-Objekte im Flash-Speicher. Um darauf zugreifen zu können, erzeugen wir ein NVS-Objekt, dem wir den Bezeichner eines Namensraums geben. Jeder Namespace kann Bezeichner-Wertpaare aufnehmen und dauerhaft vorhalten. Das testen wir doch gleich einmal.
>>> from esp32 import NVS # Klasse importieren
>>> nvs=NVS("test") # Namensraum test erstellen
>>> blob=b"Das ist ein Test" # bytes-Objekt erzeugen
>>> blob # Kontrolle
b'Das ist ein Test'
>>> nvs.set_blob("text",blob) # Name ist text, Wert ist das bytes-Objekt blob
>>> container=bytearray(30) # zum Auslesen brauchen wir ein bytes-Array
>>> nvs.get_blob("text",container) # Blob in das Array holen, 16 Bytes sind es
16
>>> print(container[:16]) # den Inhalt wiedergeben
bytearray(b'Das ist ein Test')
>>> nvs.set_i32("zahl",1234567)
>>> nvs.get_i32("zahl")
1234567
>>> nvs.commit() # Werte in den Flash schreiben
Jetzt können Sie ausschalten - nach dem nächsten Booten sind die Werte noch da.
>>> from esp32 import NVS # Klasse importieren
>>> nvs=NVS("test") # Namensraum test referenzieren
>>> nvs.get_i32("zahl")
1234567
>>> nvs.erase_key("zahl") # den Schlüssel zahl löschen
>>> nvs.get_i32("zahl")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OSError: (-4354, 'ESP_ERR_NVS_NOT_FOUND')
nvs=NVS("Quake")
Wir erzeugen ein I2C-Objekt. Und übergeben es an die OLED-Instanz d.
i2c=SoftI2C(scl=Pin(22),sda=Pin(21),freq=400000)
dheight=64
d=OLED(i2c,heightw=dheight)
d.writeAt("EARTHQUAKE 1.0",0,0)
An GPIO36 deklarieren wir die ADC-Instanz adc und stellen den am ADXL335 gemessenen Spannungsbereich ein, sowie die Auflösung auf 12 Bit. Der höchste Zählwert ist dann 4095.
adcPinNumber=36
adcPin=Pin(adcPinNumber)
adc=ADC(adcPin)
adc.atten(ADC.ATTN_11DB) # 150 - 2450 mV
adc.width(ADC.WIDTH_12BIT)
# 0...4095; LSB = 3149mV/4095cnt=0,769mV/cnt
# @313mV/g (313mV/g)/0,769mV/cnt = 408cnt/g
# LSBg = 1/408cnt/g = 2,45mg/cnt
lsbC=3149/4095 # mV / cnt
lsbG=lsbC/313 # g / mV
Mit Hilfe eines DVM und des 10-Gang-Potentiometers bestimme ich den Spannungswert, für den ich 4095 counts (count = Zählwert-Einheit) erhalte. Das Voltmeter liegt an GPIO36 und GND. Damit kann ich das LSB (=Spannungswert für 1 cnt) des ADCs berechnen.
Abbildung 6: Eichen des ADC
Abbildung 7: Erdbeben-sensor - ADC-Eichung
Am ADXL335 liegt am z-Ausgang eine Spannung von 0,313V an, wenn dieser parallel zur Tischfläche liegt. Er erfasst dann die Erdbeschleunigung g. Teile ich diese Spannung durch das LSB des ADC, dann erhalte ich die Anzahl cnt für 1g: 408 cnt/g. Der Kehrwert davon ist das LSB der Erfassung der Beschleunigung LSBg=2,45 Milli-g/cnt. Damit kann ich direkt die ADC-Werte in Beschleunigungswerte umrechnen.
Die Erdbeschleunigung interessiert mich nur am Rande, weil ich sie zur Berechnung der Beschleunigungswerte durch Bewegungen des Untergrundes brauche. Auch da begnüge ich mich mit dem ADC-Wert. Um das Rauschen (Abweichungen durch zufällige Messfehler von ADXL335 und ESP32-ADC) zu verringern, bestimme ich den Mittelwert von 1000 Einzelmessungen. s0 lege ich im NVS-Namespace Quake unter dem Schlüssel s0 ab.
s0=0
su=4095
so=0
n=1000
m=[]
for i in range(n):
s=adc.read()
m.append(s)
s0+=s
su=min(su,s)
so=max(so,s)
s0=int(s0/n)
nvs.set_i32("s0",s0)
nvs.commit()
Gleichzeitig habe ich den größten und kleinsten Messwert ermittelt. Daraus berechne ich die größte Abweichung nach unten und oben. Die Werte wandern mit print() ins Terminalfenster.
dsu=int(s0-su+1)
dso=int(so-s0+1)
dsc=max(dsu,dso)*2
ds = dsc*lsbC*lsbG
print ("s0= {}; su={}; so= {}; dsu={}; dso= {}".format(s0,su,so,dsu,dso))
print ("Rauschen: {} cnts= {:0.2f} g".format(dsc,ds))
Die Abbruchtaste ist die Flash-Taste an GPIO0, die LED an GPIO2 sagt uns, wann eine Messung läuft.
bussy=Pin(2,Pin.OUT,value=0)
taste=Pin(0,Pin.IN,Pin.PULL_UP)
Zur Verteilung der Arbeit definiere ich ein paar Funktionen. Das schafft Übersicht durch Modularisierung.
def getAdc(n):
s=0
for i in range(n):
s+=adc.read()
return int(s/n)
Auch die Messergebnisse werden durch Mittelwertbildung ein wenig geglättet. Die Funktion getS0() holt mir den s0-Wert aus dem NVS-Namespace Quake.
def getS0():
return nvs.get_i32("s0")
In der Hauptschleife wird jedes Quake-Ereignis in durchnummerierten Dateien festgehalten. readData() liest diese Dateien aus und speichert die Werte in der Liste s. Die Nummer der Datei übergebe ich beim Aufruf an den Parameter n.
def readData(n):
global s
s=[]
name="data"+str(n)+".csv"
with open(name,"r") as f:
for line in f:
zeile=line.strip()
if zeile.find(";") != -1:
nr,s1=zeile.split(";")
val=int(s1)
s.append(val)
Damit die Änderungen an der Liste s aus der Funktion nach draußen durchdringen, deklariere ich s als global. Die Liste wird geleert und der Dateiname zusammengesetzt. Die Datei öffne ich mit der with-Anweisung zum Lesen, dadurch brauche ich mich am Ende nicht um das Schließen der Datei zu kümmern.
Die for-Schleife iteriert über den gesamten Inhalt und liefert mir in line den Text einer Zeile. Der Zeilenvorschub, "\n" = ASCII 10, wird abgezwickt. Dann suche ich nach einem ";". Ist das Trennzeichen enthalten, spalte ich die Zeile in Laufindex und Wert. Den Text wandle ich in eine Ganzzahl und hänge diese als neues Listenelement an s an.
Für die Darstellung in EXCEL kann ich den Inhalt der Liste s durch convert() in trickreicher Weise vorbereiten. Über den Parameter v übergebe ich einen Teil von s oder die ganze Liste. Ich hole s0 und öffne eine Datei data.csv zum (Über-) Schreiben. Der String S0 erhält die Form ";2376". In der for-Schleife setze ich Zeilen der Form "23;2482;2376\r" zusammen und schreibe sie in die Datei. Windows braucht am Zeilenende statt des Line feeds \n einen Wagenrücklauf \r. Die so erzeugte Datei data.csv (csv = character separated Values) kann ich mit Rechtsklick vom ESP32 zum PC ins Arbeitsverzeichnis von Thonny hochladen und dort in Excel öffnen. Genaueres dazu später.
def convert(v):
s0=getS0()
with open("data.csv","w") as f:
print("Elemente:",len(v))
S0=";"+str(s0)
for n in range(len(v)):
line=str(n)+";"+str(v[n])+S0+"\r"
f.write(line)
Die Funktion, die ich am häufigsten wiederverwende, ist TimeOut(). Die Closure erzeugt mir einen nichtblockierenden Softwaretimer. Die Ablaufzeit übergebe ich als Wert in Millisekunden. Die zurückgegebene Referenz auf die Funktion compare() weise ich einfach einem Bezeichner zu. So kann ich an verschiedenen Stellen im Programm den Timerzustand prüfen – noch nicht abgelaufen: False, beendet: True.
def TimeOut(t):
start=ticks_ms()
def compare():
return int(ticks_ms()-start) >= t
return compare
Zur Darstellung der Werte im Display kann man die Funktion grafik() aufrufen. Wie bei convert() übergebe ich als Argument eine Liste oder ein Slice (Scheibe) davon. S[:50] bringt die Werte von Platz 0 bis 49, s[45:132] holt die Werte von Platz 45 bis 131. Die Variable laenge merkt sich die Anzahl der Elemente, ml kriegt die halbe Displayhöhe ab. min() und max() ermitteln den größten und kleinsten Wert. Die beiden brauche ich für die Skalierung, damit jeder Datenpunkt ins Display passt. Mit s0 und max() wird die größte Abweichung berechnet, die in die Berechnung des Skalierungsfaktors yFaktor eingeht. Die erste for-Schleife muss auf zwei Fälle reagieren können – mehr als 64 Werte oder bis zu 64 Werten in der Liste. Das klärt die konditionale Zuordnung an ziel. Bei maximal 64 Werten reicht ein Durchlauf.
Display löschen, Mittenlinie ziehen, der erste x-Wert ist 0. Der ganzzahlige Anteil der skalierten Differenz aus Messwert s1 und Ruhewert s0 wird von der halben Anzeigenhöhe subtrahiert, weil positive y-Werte oberhalb der Mittenlinie liegen müssen. Die linke obere Ecke des Displays hat die Koordinaten 0|0, die untere 0|63.
Die innere for-Schleife greift sich nun bis zu 62 Werte aus der Liste, beginnend bei der i-ten Position, welche die äußere Schleife vorgibt. Der x-Wert für den Endpunkt der Linie von x1|y1 aus ergibt sich als Differenz von j und i. Der Faktor 2 dehnt die x-Achse, weil die Kurvenlinien sonst zu dicht aufeinander folgen. Das erklärt, warum bei einem Display mit 128 Pixeln Breite der Index i nur bis maximal 64 Punkte vor Listenende läuft. Linie zeichnen und x1|y1 auf x2|y2 setzen. Bislang haben wir nur in den Puffer gearbeitet. show() schickt den Inhalt zur Anzeige.
Das Durchschieben der Werte kann ganz schön lange dauern. Das hängt davon ab, wie lang die Scandauer gewählt wird. Mit der Flash-Taste kann man den Vorgang deshalb abbrechen.
Damit stehen wir auch schon kurz vor der Hauptschleife, Dateienzähler auf 0.
Wir holen einen Wert vom ADC, berechnen die Abweichung vom Grundwert s0, der Absolutwert davon kommt nach ds. Liegt der Wert außerhalb des Grundrauschens, wird ein Scan getriggert. Den trigger-Wert schicken wir ans Terminal und ans Display. Dann wird ein Dateiname erzeugt und damit eine Datei zum Schreiben geöffnet.
n=0
while 1:
s1=getAdc(3)#adc.read()
trigger=s1-s0
ds=abs(s1-s0)
if ds > dsc :
bussy.on()
print("{}. Trigger: {}".format(n,ds))
d.writeAt("{}. Trig {}".format(n,trigger),0,4)
name="data"+str(n)+".csv"
sock.sendto((str(n)+"\r").encode(),receiver)
with open(name,"w") as f:
Wir erzeugen eine leere Liste s und stellen den Timer auf eine halbe Sekunde. over verweist jetzt auf die Funktion compare(), die wir durch over() aufrufen können. Weshalb das möglich ist, obwohl ja die Funktion TimeOut() mit der Rückgabe der Referenz auf compare() bereits beendet ist, das können Sie hier nachlesen.
s=[]
over=TimeOut(500)
Solange over() den Wert False zurückgibt, sind die 500 Millisekunden noch nicht abgelaufen. Weil unser Timer den Programmablauf nicht blockiert, können wir in der Zwischenzeit einen Haufen Dinge erledigen. Wir hängen den Messwert in die Liste, holen einen neuen, verschlafen eine Millisekunde und prüfen den Status der Flash-Taste. Ist sie in diesem Moment gedrückt, verlassen wir die Schleife
while not over():
s.append(s1)
s1=getAdc(5)
sleep(0.001)
if taste.value()==0: break
Die Anzahl der Werte wird bestimmt und uns im Terminal mitgeteilt, dann schreiben wir die Liste in die Datei und erhöhen den Zähler für den nächsten Trigger.
aw=len(s)
print("***** bitte warten {} Werte *****".\
format(aw))
for i in range(aw):
f.write("{};{}\n".format(i,s[i]))
print("=========== FERTIG ================")
n+=1
if taste.value()==0:
d.clearAll()
d.writeAt("PROGRAM",0,0)
d.writeAt("CANCELED",0,1)
sys.exit()
Mit der Flash-Taste kann man das Programm geordnet verlassen.
Mit convert() kann man eine Datei herstellen, die ohne Probleme direkt in Excel geöffnet werden kann. Im Dialog Datei öffnen wählen Sie den Typ csv. Navigieren Sie zum Arbeitsverzeichnis und öffnen Sie die Datei data.csv, die Sie mit convert() hergestellt haben.
Abbildung 8: Dateityp csv
Markieren Sie die drei Spalten A, B und C. Durch die konstanten s0-Werte in der Spalte C bekommen wir auf einfache Weise die Null-Linie. Sie entspricht der Mittenlinie im Display. Öffnen Sie jetzt das Menü Einfügen.
Abbildung 9: Spalten markieren - Einfügen
Klappen Sie das Menü Punkt-xy-Diagramme auf und klicken Sie auf Punkte mit interpolierten Linien
Abbildung 10: Punkt-xy-Diagramme
Abbildung 11: Punkte mit interpolierten Linien
Fertig!
Abbildung 12: Fertiges Diagramm
Den Start der Aufzeichnung können Sie zoomen, mit einem Rechtsklick auf das Diagramm – Daten auswählen.
Abbildung 13: Daten auswählen
Ersetzen Sie den Wert hinter C$ durch einen kleineren - OK
Abbildung 14: Ausschnitt anzeigen
Abbildung 15: Zoom auf den Startbereich
Damit sind wir für heute am Ende angekommen. Was kommt als Nächstes?
Wie wäre es mit einem Funkkontakt vom ESP32 zum PC? Oder würde es Ihnen gefallen, wenn Sie mit dem Joystick durch die Messkurven auf dem Display wandern könnten? Praktisch wäre doch sicher auch eine Aufzeichnung der Messwerte auf einer SD-Speicherkarte. Wenn dann auch noch nachträglich feststellbar wäre, wann das auslösende Ereignis stattgefunden hat, wäre das auch nicht zu verachten, oder? Weil wir bislang nur die Erfassung der vertikalen z-Richtung betrachtet haben, sollten wir vielleicht auch die x- und y-Achse mit einbeziehen, denn Erdbebenwellen können sich auch longitudinal oder transversal in der Ebene ausbreiten.
Ich mache mir schon mal ein paar Gedanken darüber.
Bis dann.
]]>
12 |
|
|
1 |
|
|
1 |
||
1 |
||
1 |
||
1 |
||
1 |
IR Fernbedienung |
|
diverse |
Es wurde ein HC-SR04 Ultraschallsensor hinzugefügt, um die Entfernung zu einem Hindernis zu erkennen. Die Versorgungsspannung von 5 VDC wird von den Batterien geliefert. Sie können statt der USB-Buchse auch den VIN-Pin, oder den Power Supply verwenden. Dann müssen Sie allerdings darauf achten, dass Sie mehr als 5V anlegen, denn der VIN-Pin ist mit einem Spannungsregler verbunden.
Der Echo-Pin des Sensors wird mit dem digitalen Pin 8 des Mikrocontrollers verbunden, der Trig-Pin mit dem digitalen Pin 9. Echo sendet einen Ultraschallimpuls, Trig empfängt ihn. Der Sensor des Infrarotempfängers KY-022 wird an den Pin 11 des Mikrocontrollers angeschlossen. Dieser Sensor empfängt das Infrarotsignal der Fernbedienung. Die notwendige Spannung für dieses Modul ist ebenfalls 5 VDC, was mit dem Netzteil oder der Batterie gespeist werden kann.
Mit der Installation des HC-SR04-Ultraschallsensors wollen wir die Eigenschaft in den Roboter implementieren, eine Aktion auszuführen, wenn er ein Hindernis in einer bestimmten Entfernung erkennt. In diesem Fall besteht die Aktion darin, sich in die Ausgangsposition von home() zu begeben, wenn ein Hindernis in einer Entfernung von weniger als 30 Zentimetern erkannt wird.
Damit unser Roboter diese Aktion ausführen kann, arbeiten wir mit dem letzten Sketch aus Teil 1, quadruped_robot_walk.ino, mit dem unser Roboter laufen konnte. Um den Ultraschallsensor zu aktivieren, wird die Bibliothek "SR04.h" am Anfang des Sketches inkludiert. Sie enthält die notwendigen Funktionen für den korrekten Betrieb des Sensors.
#include "SR04.h"
Als Nächstes müssen wir dem Mikrocontroller mitteilen, an welche Pins der Sensor angeschlossen ist. Dazu definieren wir zunächst zwei Konstanten: TRIG_PIN_FRONT an den digitalen Pin 9 und ECHO_PIN_FRONT an den digitalen Pin 8. Dann müssen wir ein Objekt aus der SR04-Bibliothek implementieren (ultrasonics_sensor_front) und als Parameter die Namen der Konstanten übergeben. Um die Entfernungsdaten zu speichern, werden wir eine globale Variable vom Typ long definieren.
#define TRIG_PIN_FRONT 9 #define ECHO_PIN_FRONT 8 SR04 ultrasonics_sensor_front = SR04(ECHO_PIN_FRONT,TRIG_PIN_FRONT); long front_distance;
Damit sollte nun der Sensor funktionstüchtig sein. Wenn Sie Probleme haben, sollten Sie sich einen kurzen Sketch schreiben und die Sensorwerte ausgeben lassen. In der setup()-Methode müssen wir in Bezug auf diesen Sensor keine Änderungen vornehmen.
Vielleicht erinnern Sie sich, dass wir in der loop() die Funktion walk() aufgerufen haben. An dieser Stelle müssen wir nun eine Bedingung einfügen. Sobald ein Hindernis erkannt wird, soll der Roboter anhalten. Mit der erweiterten Bedingung if-else sorgen wir dafür, dass er läuft, oder sich bewegt.
Zuerst rufen wir in der loop() die Funktion front_distance_object() auf.
front_distance_object();
Darin wird die Funktion Distance() des ultrasonics_sensor_front-Objekts unseres HC-SR04-Sensors ausgeführt. Diese Funktion wandelt das vom TRIG-Pin des Sensors empfangene Signal in eine Entfernung in Zentimetern um und speichert diesen Wert in der globalen Variable front_distance,. Anschließend wird der Entfernungswert über den seriellen Monitor mit der Funktion Serial.print() angezeigt. Die Ausgabe dient nur für Testzwecke. Die Ausgabe auf dem Seriellen Monitor können Sie später auch auskommentieren oder entfernen.
void front_distance_object() { front_distance = ultrasonics_sensor_front.Distance(); Serial.print("Distance to front obstacles "); Serial.print(front_distance); Serial.println(" cm"); delay(100); }
Nach der Rückkehr zur loop()-Funktion enthält die globale Variable front_distance einen Wert, der für die nachfolgende Bedingung verwendet wird. Wir vergleichen mit dem numerischen Wert 30 (also 30 cm). Abhängig von diesem Abstand wird walk(), oder home() aufgerufen. Der Roboter läuft, oder bleibt stehen.
if (front_distance >= 30) { walk(); } else { home();}
Das alles läuft in der loop() durchgehend. Mit jedem Durchlauf wird die Entfernung neu gemessen und dann neu entschieden, ob gelaufen oder stehen geblieben wird.
Der Rest des Codes sind die Funktionen walk() und home(), die bereits in Teil 1 beschrieben wurden.
Download quadruped_robot_walk_sonic.ino
Im Moment läuft der Roboter und wenn er ein Hindernis entdeckt, bleibt er stehen. Wir wollen das Projekt nun um die IR-Fernbedienung erweitern. Dafür nutzen wir das IR-Sensor-Modul. Mit der Fernbedienung werden wir durch drücken einer Zahl eine bestimmte Aktion ausführen. Wir nutzen die vorangegangen Sketches und fassen sie alle in diesem einen Sketch zusammen. Dann können wir alle einzelnen Bewegungen mit der Fernbedienung ausführen.
Beginnen wir mit dem Einbinden des IR-Sensors. Auch hierfür wird die passende Bibliothek IRremote.h inkludiert und ein Objekt daraus instanziiert. Wir benötigen dann noch eine Variable, in der wir die Zahl der Fernbedienung speichern. Abhängig von diesem Wert werden später die dazugehörigen Aktionen ausgeführt.
Weiterhin benötigen wir die Bibliotheken für den Motortreiber, die I2C-Kommunikation und den Ultraschallsensor.
#include <Wire.h> #include <Adafruit_PWMServoDriver.h> #include "IRremote.h" #include "SR04.h"
Wir der Motortreiber angesprochen wird, kennen Sie aus Teil 1. Wir deklarieren noch zwei Konstanten für den Maximalwert der Servos und die Bewegungsgeschwindigkeit, die durch Pausen realisiert wird.
Adafruit_PWMServoDriver servoDriver_module = Adafruit_PWMServoDriver(); #define SERVOMIN 100 #define SERVOMAX 500 int movement_speed = 50;
Wir deklarieren eine Variable mit der Pinnummer, an die der IR-Sensor angeschlossen ist. Außerdem instanziieren wir ein Objekt aus der IRremote-Bibliothek, der wir diesen Pin übergeben. Als Nächstes brauchen wir dann noch eine Variable, in der wir die gedrückte Taste speichern. Das ist ein Objekt der Klasse decode_results, deren Adresse später der Decoder-Methode übergeben werden kann.
int receiver_IR_module = 11; IRrecv action_selected(receiver_IR_module); decode_results number;
In den nächsten vier Zeilen nutzen wir den gleichen Code wie bereits zuvor, um den Ultraschallsensor verwenden zu können. Trigger- und Echo-Pin, sowie ein SR04-Objekt und die front-distance-Variable, in der die Entfernung gespeichert wird.
#define TRIG_PIN_FRONT 9 #define ECHO_PIN_FRONT 8 SR04 ultrasonics_sensor_front = SR04(ECHO_PIN_FRONT,TRIG_PIN_FRONT); long front_distance;
In der setup()-Funktion werden der serielle Monitor und das Servotreiber-Modul initialisiert. Die PWM-Frequenz der Motoren beträgt 50 Hz. Dann wird mit der home()-Funktion der Roboter in seine Grundposition gebracht. Diese übernehmen wir auch aus dem ersten Teil. Zum Schluss initialisieren wir noch das Infrarotmodul.
void setup() { Serial.begin(9600); servoDriver_module.begin(); servoDriver_module.setPWMFreq(50); home(); action_selected.enableIRIn(); }
In der loop()-Funktion wird regelmäßig geprüft, welcher Wert vom Infrarotsensor empfangen wird. Die Funktion decode() bekommt die Adresse des decode_results-Objekts übergeben und prüft in der if-Anweisung, ob es sich bei dem empfangenen Wert um eine Zahl handelt. Wenn ja, wird translateIR() aufgerufen und anschließend der IR-Empfang zurückgesetzt, um einen neuen Wert zu erhalten. Wurde keine Zahl erkannt, wird trotzdem translateIR() aufgerufen, jedoch mit dem alten Wert.
void loop() { if (action_selected.decode(&number)) { translateIR(); action_selected.resume(); } else { translateIR(); } }
In der translateIR()-Funktion wird eine switch-case-Anweisung ausgeführt. Abhängig vom empfangenen Wert der Fernbedienung werden damit die verschiedenen Funktionen für die Bewegung des Roboters aufgerufen.
void translateIR() { switch(number.value) { case 0xFF6897: home(); break; case 0xFF30CF: pushups(); break; case 0xFF18E7: shoulder(); break; case 0xFF7A85: walk(); break; default: Serial.println("other button"); } }
Die Funktionen, die hier aufgerufen werden, wurden in Teil 1 dieses Artikels beschriebenen, nämlich: home(), pushups(), shoulder() und walk(). Die Zahlen, die zur Ausführung der oben genannten Methoden gedrückt werden, sind:
WICHTIGER HINWEIS: Die hexadezimalen Werte der Fernbedienungstasten ändern sich je nach Hersteller. Die Hexadezimalzahlen der Fernbedienung des Electronics Super Starter Kit sind:
Sollten Sie eine andere Fernbedienung verwenden, nutzen Sie einen Sketch, der Ihnen die Werte im Seriellen Monitor anzeigt und übernehmen Sie diese für Ihren Quellcode.
Von den vier aufgerufenen Funktionen haben sich weder home(), noch pushups(), noch shoulder() im Vergleich zu dem, was in Teil 1 des Projekts erklärt wurde, verändert. Die walk()-Methode wird durch die Entfernungsmessung mit dem SR04-Ultraschallsensor erweitert. Ich zeige Ihnen nun noch, wie ich das in diesen Sketch implementiert habe.
Zuerst wird front_distance_object() aufgerufen, die die Entfernung zum nächsten frontalen Hindernis misst und den Wert in der globalen Variablen front_distance speichert. Es wird nun wieder verglichen, ob der Wert der größer oder gleich 30 (cm) ist. Ist die Bedingung wahr, wird die Bewegung ausgeführt. Ansonsten wird im else-Zweig die home()-Funktion aufgerufen und der Roboter bleibt stehen, bis das Hindernis außer Reichweiter ist.
void walk() { front_distance_object(); if (front_distance >= 30) { servoDriver_module.setPWM(0, 0, 450); delay(movement_speed); . . . . . . . . . . . . . . . servoDriver_module.setPWM(4, 0, 400); delay(movement_speed); } else { home(); } }
Damit sich der Roboter nun frei bewegen kann, habe ich zwei Akkupacks installiert und parallelgeschaltet. Das ergibt einen Strom von 2A bei 5 VDC. Ich habe sie, wie oben im Schaltbild zu sehen ist, an den USB-Port angeschlossen.
Somit sind wir nun am Ende des Projekts angekommen. Ich lade Sie herzlich dazu ein, das Projekt zu erweitern. Zum Beispiel könnten Sie Bewegungssequenzen implementieren, die die Richtung des Roboters ändern. Dadurch könnte er dann autonom den Hindernissen ausweichen.
Wir hoffen, dass Ihnen dieses Projekt gefallen hat.
]]>In diesem Projekt werden wir einen vierbeinigen, beweglichen Roboter bauen und damit zeigen, dass der Fantasie keine Grenzen gesetzt sind. Alles ist eine Frage der Beharrlichkeit und der Suche nach einem Weg, Ideen in die Realität umzusetzen.
Wir wollen die Basis der Konstruktion und der Programmierung der Bewegungen zeigen. Dann ist es eine Frage der Vorstellungskraft, wie wir die Bewegungen erweitern und welche zusätzlichen Module wir installieren können, um diesen Roboter zu verbessern.
Dieser Artikel ist in zwei Teile aufgeteilt. In Teil 1 werden wir den Roboter zusammenbauen und die ersten Bewegungen ausführen. In Teil 2 werden wir Befehle über eine Infrarot-Fernbedienung senden, eine Hinderniserkennung hinzufügen und eine Stromversorgung mit Batterien, um den Roboter frei bewegen zu lassen.
12 |
|
|
1 |
|
|
1 |
||
1 |
||
1 |
||
1 |
||
1 |
IR Fernbedienung |
|
diverse |
Für das Skelett unseres Roboters werden wir 3 mm dickes Balsaholz verwenden. Es hat ein geringes Gewicht und es ist damit einfacher, Änderungen an den Teilen vorzunehmen. Folgend finden Sie die Pläne der Teile mit ihren Maßen.
Bevor wir mit dem Zusammenbau des Roboters beginnen, müssen wir alle Servomotoren auf 90 Grad einstellen, da dies der Mittelpunkt ihrer Bewegung ist. Alle Teile werden in dieser Mitte zusammengebaut. Auf diese Weise haben wir eine erste visuelle Referenz des Zustands der Beine und es wird einfacher sein, die Einstellungen der Servomotoren vorzunehmen, um die Werte der Startposition des Roboters festzulegen, die wir im Sketch home.ino programmieren werden.
Wir fangen mit dem Schulterblatt an. Diese Teile sind ein Würfel mit 31 mm Kantenlänge. Eine der Seiten des Würfels ist unbedeckt, das ist die Seite, die zum Inneren des Körpers hin positioniert wird. Die obere und die untere Seite haben eine Wandstärke von 3 mm, während die beiden Seiten mit den Teilen, die mit der Servomotorwelle verschraubt werden, 6 mm dick sind. Die Fläche, die die Drehachse des Roboterkörpers enthält, ist ebenfalls 6 mm dick. Bevor wir die Teile des Servomotors anbringen, werden wir die Schulterblätter auf dem Körper anschauen. Wir wollen sicherstellen, dass wir sie richtig montieren, da die äußere Fläche jedes Schulterblattes das Teil enthalten muss, das an jedes obere Teil des Beins geschraubt wird.
Das Stück des Unterteils des Beins hat eine Dicke von 6 mm, im oberen Teil werden wir das Teil installieren, das an die Welle des Servomotors geschraubt wird. Im unteren Teil des Beins werden wir einige Gummistücke einkleben, damit es Halt auf dem Boden hat. Hier ist es auch notwendig, auf die Installation des Verbindungsstücks zum Servomotor zu achten, da es im unteren Beinteil auf der rechten Seite in einer Seite und in denen auf der linken Seite in der gegenüberliegenden Seite installiert wird.
Der obere Teil des Beins hat auch eine Dicke von 6 mm. Wir müssen eine Aussparung schneiden, um den Servomotor einfügen zu können. Wir müssen genau aufpassen, denn das Oberteil auf der rechten Seite hat die Aussparung auf einer Seite und die beiden auf der linken Seite auf der gegenüberliegenden Seite. Der obere Servomotor betätigt das Oberteil, während der untere Servomotor das untere Teil jedes Beins betätigt.
Der Körper des Roboters ist etwas komplizierter, da wir vier Servomotoren installieren müssen, auf deren Achse die Nabe des Schulterblattes geschraubt wird. Auf der gegenüberliegenden Seite des Servomotors, der das Schulterblatt bewegt, müssen wir eine Welle von der Nabe zum Loch in der Körperhalterung einführen, die mit der Servomotorwelle übereinstimmen muss, damit sich das Schulterblatt reibungslos bewegt. Die Welle muss eine geeignete Dicke haben, damit sie sich durch die Bewegung nicht verformt und das Gewicht des Roboters tragen kann. In unserem Prototyp hat diese Welle eine Dicke von 6 mm.
Nachdem alle Teile gebaut sind, beginnen wir mit dem Zusammenbau des Roboters. Als erstes werden wir zwei Servomotoren in jedes Oberteil und die vier Servomotoren in den Körper des Roboters einbauen. Sobald wir alle Servomotoren eingebaut haben, müssen wir jedes Bein in das entsprechende Schultergelenk einbauen. Wir haben zuvor eine Voreinstellung aller Servomotoren auf 90 Grad vorgenommen. Mit diesen Positionen müssen wir die Nabe auf den Körper montieren, indem wir sie auf die Welle des Servomotors setzen, so dass sie vollständig mit dem Körper übereinstimmt. Wir schrauben die Nabe auf die Welle des Servomotors und dann setzen wir die Welle ein, indem wir sie durch die Körperstütze und bis zur Nabe führen. Dieses Verfahren wird für die vier Schulterblätter unseres Roboters durchgeführt.
Nachdem wir die mechanischen Teile des Roboters zusammengebaut haben, installieren wir die Abdeckung des Körpers und darauf die PCA9685-Karte samt AZ-ATmega328-Mikrocontroller mit dem Shield. Mit dem Prototype Shield sind Sie auch in der Lage, Ihr Projekt nach Ihren Bedürfnissen zu erweitern. Sie sehen hier schon das installierte IR-Empfänger Modul, das wir in Teil 2 verwenden werden.
Wir stellen alle Verbindungen des Moduls und der Servomotoren her, wie sie oben im elektronischen Schaltplan dargestellt sind. Dann beginnen wir mit der Programmierung.
Als Aktuatoren für die Bewegungen verwenden wir 12 MG90S-Servomotoren, deren Getriebe aus Metall bestehen und sie dadurch einen besseren mechanischen Widerstand haben. Zur Steuerung der Motoren verwenden wir das Modul "PCA9685 16 Channel 12 bit PWM Servo Driver", das zwei wichtige Funktionen hat: erstens die Versorgung der Servomotoren mit 5 Vdc von einer externen Stromversorgung und zweitens die Ansteuerung jedes Servomotors über den entsprechenden PWM-Pin mit den vom Mikrocontroller über den I2C-Port übermittelten Daten. Es werden nur zwei Pins für die Kommunikation zwischen dem Mikrocontroller und dem PCA9685-Modul verwendet, dadurch bleibt Platz für spätere Erweiterungen.
Im Sketch legen wir die Position fest, die jeder Servomotor einnehmen muss. Die kombinierten Drehungen der Motoren führen zur Bewegungssequenz des Roboters. Der Mikrocontroller sendet die jeweiligen Positionen der Servos an das Motortreiber Modul über die I2C-Schnittstelle.
Ein mühsamer Teil dieses Projekts ist die Einstellung der Werte für jede Position der Servomotoren jedes Beins für die koordinierte Bewegungssequenz und damit den Roboter zum Laufen zu bringen. In diesem Teil 1 des Projekts zeige ich drei Sketches mit einigen Grundbewegungen und einen weiteren Sketch mit der Ausgangsposition des Roboters.
Um die Roboterkomponenten in diesem Teil 1 des Projekts mit Spannung zu versorgen, verwenden wir eine feste 5-VDC-Quelle. Da der Mikrocontroller über die USB-Schnittstelle mit der Arduino IDE verbunden bleibt, erhält er darüber seine Spannung.
In Teil 2 des Artikels werden wir den HC-SR04-Ultraschallsensor hinzufügen, der die Entfernung zu jedem Hindernis vor dem Roboter misst. Außerdem werden wir das IR-Empfängermodul und eine IR-Sender-Fernbedienung verwenden, um Befehle zu erteilen und schließlich werden wir Batterien für die Stromversorgung installieren, damit sich der Roboter frei bewegen kann.
Dies ist die Anfangsposition des Roboters. Wie Sie sehen können, ist der Code dieses ersten Sketches sehr einfach und führt nur die anfängliche Positionierungsbewegung der Servomotoren aus. Die ersten beiden Zeilen des Codes sind die Bibliotheken, die wir benötigen, um den Sketch korrekt auszuführen. Wire.h ist die Bibliothek für die I2C-Kommunikation und Adafruit_PWMServoDriver.h ist für die Verwendung des PCA9685-Moduls notwendig.
#include <Wire.h> #include <Adafruit_PWMServoDriver.h>
Die nächsten drei Zeilen sind der Reihe nach die Implementierung eines Adafruit_PWMServoDriver-Objekts zur Steuerung der Servomotorpositionen und die Variablen SERVOMIN und SERVOMAX, die die Werte der steigenden und fallenden Flanke für die 0-Grad- bzw. 180-Grad-Positionen der Servomotoren darstellen.
Adafruit_PWMServoDriver servoDriver_module = Adafruit_PWMServoDriver(); #define SERVOMIN 100 #define SERVOMAX 500
In der setup()-Methode initialisieren wir zuerst den seriellen Monitor und in der nächsten Zeile das PCA9685-Modul durch sein zuvor implementiertes Objekt. Mit der Funktion begin() geben wir die Arbeitsfrequenz der Servomotoren mit dem Aufruf setPWMFreq(50) an, die 50Hz beträgt. In der letzten Zeile rufen wir die home()-Funktion auf.
void setup() { Serial.begin(9600); servoDriver_module.begin(); servoDriver_module.setPWMFreq(50); home(); }
Analysieren wir die erste Zeile der Funktion home(). Der erste Servomotor, der seine Position ändern wird, ist der mit der Nummer 0, der zum Unterteil des linken Vorderbeins gehört. Mit dem Aufruf setPWM(0, 0, 420) des Objekts servoDriver_module geben wir dem Modul PCA9685 an, dass es den an seinem Port 0 angeschlossenen Servomotor bewegt und ihn in die Position bringen soll, die sich aus der Subtraktion des Wertes der steigenden Flanke (0) von der fallenden Flanke (420) ergibt.
void home() { servoDriver_module.setPWM(0, 0, 420); servoDriver_module.setPWM(1, 0, 360); servoDriver_module.setPWM(2, 0, 350); servoDriver_module.setPWM(4, 0, 400); servoDriver_module.setPWM(5, 0, 440); servoDriver_module.setPWM(6, 0, 280); servoDriver_module.setPWM(8, 0, 240); servoDriver_module.setPWM(9, 0, 220); servoDriver_module.setPWM(10, 0, 340); servoDriver_module.setPWM(12, 0, 190); servoDriver_module.setPWM(13, 0, 260); servoDriver_module.setPWM(14, 0, 260); delay(10000); }
Die darauffolgenden Zeilen bewegen die anderen Motoren nach dem gleichen Prinzip. Im Quellcode ist beschrieben, welcher Motor für welches „Körperteil“ zuständig.
In diesem Sketch bleibt die loop()-Funktion leer, da nur die Startpositionen beim Aufruf der setup()-Funktion und damit beim Start des Mikrocontrollers, eingestellt werden.
Download quadruped_robot_home.ino
Mit diesem Sketch wird der Roboter einige kleine Bewegungen durchführen ausgelöst durch die Motoren an den unteren und oberen Beinteilen. In der loop() wird nun pushups() aufgerufen. Dadurch bewegt sich der Roboter immer wieder hoch und runter.
Die 12 Servomotoren ändern ihre Position und kehren dann in die Ausgangsposition von home() zurück. Die Codezeilen ähneln denen, die wir bereits gesehen haben. Die Funktion servoDriver_module.setPWM() hat als Parameter die Nummer des zu bewegenden Servomotors und die neue Position.
Download quadruped_robot_pushups.ino
Wie die Bewegungen der Beinteile in Abhängigkeit von der Veränderung der Position der Servomotoren funktioniert, zeigt die folgende Grafik.
Mit diesem Sketch werden Bewegungen in den Servomotoren der Schulterblätter des Roboters ausgeführt. Die Codezeilen sind wieder sehr ähnlich. Von der loop() aus rufen wir dieses Mal kontinuierlich die shoulder()-Funktion auf. Sie führt eine Bewegung von den home()-Positionen der Schulter-Servomotoren zu den neuen Positionen aus. Auch hier sind diese Positionen mit dem Aufruf servoDriver_module.setPWM() vorgegeben.
Download quadruped_robot_shoulder.ino
Der letzte Sketch in diesem ersten Teil ermöglicht es dem Roboter zu laufen. Dafür muss eine Sequenz programmiert werden, die einerseits die passenden Bewegungen der Beine durchführt und andererseits für das Halten des Gleichgewichts des Roboters sorgt.
Der erste Unterschied zu den anderen Sketches ist die Variablendeklaration namens int movement_speed mit einem Wert von 50. Dieser wird verwendet, um zwischen den Bewegungen der Servomotoren eine Pause von 50 Mikrosekunden einzulegen.
int movement_speed = 50;
Von der loop() aus rufen wir kontinuierlich die walk()-Funktion auf, die die Bewegungsbefehle an die Servomotoren über das PCA9685-Modul sendet. Mit servoDriver_module.setPWM() werden auch hier wieder die benötigten Parameter übergeben. Anhand des oben eingefügten Diagramms können Sie die Werte den Bewegungen besser zuordnen.
Wir beginnen in der Ausgangsposition home(), die in setup() aufgerufen wird. Dann machen wir mit dem linken Vorderbein einen Schritt nach vorne. Dazu müssen wir das untere Beinteil mit dem Servomotor 0 anheben, das obere Teil mit dem Servomotor 1 nach vorne bewegen und das untere Teil mit dem Servomotor 0 absenken, um das Bein auf dem Boden abzusetzen.
servoDriver_module.setPWM(0, 0, 450); delay(movement_speed); servoDriver_module.setPWM(1, 0, 330); delay(movement_speed); servoDriver_module.setPWM(0, 0, 420); delay(movement_speed);
Das nächste Bein, das einen Schritt nach vorne machen muss, ist das rechte Hinterbein, damit der Roboter das Gleichgewicht halten kann. Dazu führen wir die gleichen Bewegungen wie beim vorherigen Bein aus. Servomotor 8 wird angehoben, Servomotor 9 nach vorne bewegt und Servomotor 8 abgesenkt.
servoDriver_module.setPWM(8, 0, 210); delay(movement_speed); servoDriver_module.setPWM(9, 0, 250); delay(movement_speed); servoDriver_module.setPWM(8, 0, 240); delay(movement_speed);
Die nächste Aktion besteht darin, den Körper des Roboters vorwärts zu bewegen. Hier werden nur die Servos in den Schultern bewegt. Sie werden nach hinten bewegt, was dazu führt, dass sich der Körper des Roboters vorwärtsbewegt, während die Beine auf dem Boden ruhen.
servoDriver_module.setPWM(13, 0, 230); servoDriver_module.setPWM(1, 0, 360); servoDriver_module.setPWM(5, 0, 470); servoDriver_module.setPWM(9, 0, 220); delay(movement_speed);
Jetzt müssen wir die beiden anderen Beine, das linke Hinterbein und das rechte Vorderbein, vorwärtsbewegen. Wir beginnen mit dem rechten Vorderbein. Die Bewegungen sind die gleichen wie die beiden vorherigen. Wir heben mit Servomotor 12, wir schieben mit Servomotor 13 vor und wir senken mit Servomotor 12.
servoDriver_module.setPWM(12, 0, 140); delay(movement_speed); servoDriver_module.setPWM(13, 0, 260); delay(movement_speed); servoDriver_module.setPWM(12, 0, 190); delay(movement_speed);
Nun noch das linke Hinterbein. Anheben mit Servomotor 4, Vorschieben mit Servomotor 5 und Absenken mit Servomotor 4. Zwischen allen Bewegungen befindet sich eine Pause von 50 ms.
servoDriver_module.setPWM(4, 0, 450); delay(movement_speed); servoDriver_module.setPWM(5, 0, 440); delay(movement_speed); servoDriver_module.setPWM(4, 0, 400); delay(movement_speed);
Am Ende dieser Bewegungssequenz kehren die Motoren zu ihrer home()-Position zurück. Durch den wiederkehrenden Aufruf von walk() in der loop(), bewegt sich der Roboter dauerhaft vorwärts.
Das alles dient als Basis, um den Roboter wie gewünscht bewegen zu können.
Wenn Sie an dieser Stelle kein Video sehen, oder es nicht abspielen können, ändern Sie bitte die Cookieeinstellungen Ihres Browsers.
]]>Beim Theremin hatte ich den Time of Flight-Sensor VL53L0X zum Verstellen der Tonhöhe eines Synthesizers benutzt. Entfernungswerte wurden in die Frequenzen eines PWM-Signals umgesetzt. In diesem Projekt wollen wir die relativ genaue Entfernungsmessung per se nutzen. Abgesehen von dem sehr umfangreichen Treiber-Modul des VL53L0X, das wir aber nicht im Detail beleuchten wollen, ist es ein recht einfaches Projekt mit Ausbaupotenzial.
Außerdem möchte ich einmal eine ganz andere Art von Ausgabemedium verwenden. Zu dem OLED-Display, dem VL53L0X und dem ESP32 wird sich noch eine vierte Baugruppe gesellen, mit der eine Sprachausgabe möglich wird. Daneben werde ich ein wenig aus der Werkstatt plaudern. Es gab bei der Entwicklung nämlich ein vertracktes Problem, nicht hausgemacht, sondern vom MicroPython-Entwickler-Team aufs Auge gedrückt.
Willkommen bei einer neuen Folge der Reihe
heute
ToF ist das Akronym für Time of Flight, und das Modul VL53L0X Time-of-Flight (ToF) Laser Abstandssensor macht nichts anderes, als die Flugzeit eines von ihm ausgesandten IR-Laserimpulses bis zur Reflexion an einem Hindernis und zurück zu messen.
Das gelingt bei den meisten festen Oberflächen, wenn sie im IR-Bereich genügend Licht reflektieren. Wieviel das ist, können wir mit bloßem Auge leider nicht wahrnehmen. Bestenfalls kann das eine IR-Kamera, oder eine umgebaute Web-Cam, aus der das IR-Filter herausoperiert wurde.
Als Reflektor können aber auch ganz einfach Leute dienen, zum Beispiel beim Passieren einer Tür. Um genau diese Anwendung geht es in diesem Beitrag. Der Aufbau soll die Größe von Personen feststellen und über einen Lautsprecher an einem Mini-MP3-Player das Ergebnis mitteilen.
1 |
oder ESP32 NodeMCU Module WLAN WiFi Development Board oder NodeMCU-ESP-32S-Kit |
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
2 |
Widerstand 1kΩ |
2 |
Widerstand 2,2kΩ |
Der Aufbau gestaltet sich sehr einfach, Sensor und OLED-Display werden beide am gemeinsamen I2C-Bus angeschlossen. Den Chefposten übernimmt ein ESP32. Für dessen Einsatz bedarf es zweier Breadboards, die mit einer Stromschiene in der Mitte verbunden werden, damit man mit den Abständen der Pinreihen hinkommt und am Rand auch noch Kabel gesteckt werden können.
Abbildung 1: Body-Size-Meter - Aufbau
Wegen des sehr umfangreichen VL53L0X-Treibermoduls scheidet der Einsatz eines ESP8266 aus. Aber nicht nur aus diesem Grund ist der kleine Bruder des ESP32 ungeeignet, er kann nämlich auch keine zweite UART-Schnittstelle bieten, und die brauchen wir zur Ansteuerung des DF-Players.
UART ist das Akronym für Universal Asynchronous Receiver / Transmitter. Über so ein Interface sprechen wir auch mit unserem Controller vom Terminal aus in Thonny. Der ESP32/ESP8266 ist über das USB-Kabel mit dem PC verbunden und ein USB-RS232-TTL-Konverter gibt den Datenverkehr an die Schnittstelle UART0 des Controllers weiter. Deshalb können wir UART0 auch nicht benutzen, um Befehle an den Mini-MP3-Player zu senden und Daten von ihm zu empfangen. Der ESP8266 hat zwar eine zweite UART-Schnittstelle, aber das ist eigentlich nur eine halbe, weil nur eine TXD-Leitung (Sendeleitung) und keine RXD-Leitung (Empfangsleitung) zur Verfügung steht.
Fürs Flashen und die Programmierung des ESP32:
Thonny oder
Bitte unbedingt die hier angegebene Version flashen, weil sonst die VL53L0X -Steuerung nicht korrekt funktioniert.
VL53L0X.py modifiziertes Treibermodul für den ToF-Sensor auf ESP32(S) adaptiert
ssd1306.py Hardwaretreiber für das OLED-Display
oled.py API für OLED-Displays
distance.py Testprogramm für den VL53L0X
bodysize.py Betriebssoftware
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 05.02.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Abbildung 2: Body-Size-Meter - Schaltung
Weil der DF-Player mini mit 5 Volt versorgt werden muss und daher auch auf den Leitungen TX und Bussy 5V-Pegel anliegen, der ESP32 aber nur 3,3V an seinen GPIOs verträgt, brauchen wir dort Spannungsteiler, welche die Spannung auf ein verträgliches Maß reduzieren. Ich habe 1kΩ und 2,2kΩ gewählt. Sie können auch andere Werte nehmen, die etwa im Verhältnis 1:2 stehen.
Wenn die Lautsprecher mit der schwarzen Leitung an GND gelegt werden, fließt im 5V-Kreis ein Strom von ca. 450mA! Das liegt daran, dass die Anschlüsse SP1 und SP2 5V-Pegel führen. Schließen Sie daher die beiden Speaker nur an SP1 und SP2 an, nicht an GND. Auch wenn Sie nur einen Lautsprecher verwenden, gehören dessen Leitungen an SP1 und SP2. Andernfalls müsste man die Lautsprecher über einen Elko anschließen.
Abbildung 3: VL53L0X - Laser-Entfernungsmesser
Das Datenblatt für den VL53L01 liefert nicht, wie üblich, eine Register-Map und eine Beschreibung für die zu setzenden Werte. Stattdessen wird eine API beschrieben, die als Interface zum Baustein dient. Ellenlange Bezeichner in einer bunten Vielfalt prasseln auf den Leser herunter. Das hat meinen Eifer, ein eigenes Modul zu schreiben, sehr rasch auf null heruntergeschraubt. Glücklicherweise bin ich, nach ein bisschen Suchen, auf Github fündig geworden. Dort gibt es ein Paket, das für einen wipy geschrieben wurde, mit einer Readme-Datei, einem Beispiel zur Anwendung und mit dem sehr umfangreichen Modul VL53L0X.py (648 Zeilen). Der erste Start der Beispieldatei main.py brachte eine Ernüchterung, obwohl ich das Modul zum ESP32S hochgeladen und die Pins für den ESP32 umgeschrieben hatte.
Das Original
from machine import I2C
i2c = I2C(0)
i2c = I2C(0, I2C.MASTER)
i2c = I2C(0, pins=('P10','P9'))
wurde zu
from machine import SoftI2C
i2c = I2C(0,scl=Pin(21),sda=Pin(22))
Der MicroPython-Interpreter meckerte ein fehlendes Chrono-Objekt an, im Modul VL53L0X.VL53L0X in Zeile 639. Nachdem das Modul für einen anderen Port geschrieben ist, wipy devices made by Pycom, konnte es gut sein, dass noch mehr derartige Fehler auftauchen würden. Andere Firmware, andere Bezeichner für GPIO-Pins, andere Einbindung von Hardware-Modulen des ESP32…
Nachdem aber außer den I2C-Pins keine weitere Verbindung zwischen ESP32 und VL53L01 bei dem Beispiel gebraucht wurde, stufte ich die Wahrscheinlichkeit weiterer Problemstellen als gering ein. Ich nahm mir also die Methode perform_single_ref_calibration() im Modul VL53L0X.VL53L0X im Editor vor. Der Name Chrono deutete auf ein Zeit-Objekt hin.
Und tatsächlich, in der Firmware des WiPy ist die Benutzung der Hardware-Timer anders gelöst, als beim nativen ESP32. Es geht aber im Prinzip nur um die Initialisierung und Überprüfung eines Timeouts.
from machine import Timer
...
...
def perform_single_ref_calibration(self, vhv_init_byte):
chrono = Timer.Chrono()
self._register(SYSRANGE_START, 0x01|vhv_init_byte)
chrono.start()
while self._register((RESULT_INTERRUPT_STATUS & 0x07) = 0):
time_elapsed = chrono.read_ms()
if time_elapsed > _IO_TIMEOUT:
return False
self._register(SYSTEM_INTERRUPT_CLEAR, 0x01)
self._register(SYSRANGE_START, 0x00)
return True
Dafür habe ich aber meine eigene Softwarelösung. Eine Funktion, Verzeihung, eine Methode, wir bewegen uns ja in der Definition einer Klasse, also eine Methode TimeOut(), die eine Zeitdauer in Millisekunden nimmt und die Referenz auf eine Funktion in ihrem Inneren zurückgibt. So ein Konstrukt nennt man Closure. Damit habe ich nun einfach die Zeitsteuerung ersetzt, kürzer aber genauso effektiv.
def TimeOut(self,t):
start=time.ticks_ms()
def compare():
return int(time.ticks_ms()-start) >= t
return compare
def perform_single_ref_calibration(self, vhv_init_byte):
self._register(SYSRANGE_START, 0x01|vhv_init_byte)
chrono = self.TimeOut(_IO_TIMEOUT)
while self._register((RESULT_INTERRUPT_STATUS & 0x07) == 0):
if chrono() :
return False
self._register(SYSTEM_INTERRUPT_CLEAR, 0x01)
self._register(SYSRANGE_START, 0x00)
return True
Außerdem machte sich Erleichterung breit, als sich beim erneuten Start des Demoprogramms keine weitere Fehlermeldung mehr zeigte. Im Gegenteil, im Terminal wurden nach dem Start meines Testprogramms distance.py lauter nette Entfernungswerte in cm ausgegeben.
# distance.py
import sys
from time import sleep_ms,ticks_ms, sleep_us, sleep
from machine import SoftI2C, Pin
from oled import OLED
from ssd1306 import SSD1306_I2C
import VL53L0X
# ************* Objekte und Variablen erzeugen **************
SCL=Pin(22)
SDA=Pin(21)
i2c=SoftI2C(SCL,SDA)
d=OLED(i2c, heightw=32)
taste=Pin(0,Pin.IN)
tof = VL53L0X.VL53L0X(i2c)
tof.set_Vcsel_pulse_period(tof.vcsel_period_type[0], 18)
tof.set_Vcsel_pulse_period(tof.vcsel_period_type[1], 14)
tof.start()
print("started")
distance=9999
tof.read()
d.clearAll()
d.writeAt("BODY SIZE",2,0,False)
d.writeAt(" METER ",2,1,False)
d.writeAt("{:.1f} cm ".format(distance),4,3)
sleep(2)
while True:
gc.collect()
distance=0.0
n=10
for i in range(n):
distance += tof.read()
distance /= (n*10) # cm
d.writeAt("{:.1f} cm ".format(distance),4,2)
print (distance)
sleep(1)
# Schleife beenden
if taste.value()==0:
d.clearAll()
d.writeAt("BODY SIZE",4,0,False)
d.writeAt(" METER ",4,1,False)
d.writeAt("TERMINATED",3,2)
sys.exit()
Bis hierher lief alles recht gut. Aber als ich dann auf ein größeres Breadboard umgestiegen bin, um die weiteren BOBs (Break-Out-Boards) unterbringen zu können, wäre ich bald verzweifelt. Auf dem zweiten Board saß bereits ein ESP32 drauf, also sparte ich mir das Umstecken. Aber alles, was grade noch perfekt gelaufen war, funktionierte nun nicht mehr, der VL53L0X brachte nur noch völlig unbrauchbare Werte. Nach diversen ergebnislosen Versuchen, ich tauschte den VL53L0X, setzte einen weiteren ESP32 ein, nahm statt eines ESP32 Dev Kit V2 ein V4, kontrollierte und verglich mehrmals die beiden Aufbauten, prüfte die Kontakte, stets das Gleiche, keine ordentliche Funktion. Der I2C-Bus funktionierte wohl normal, der VL53L0X meldete sich zum Dienst. Das war's dann aber auch schon. Als ich doch noch den ESP32 vom ersten Board nahm, kam die Erleuchtung. Siehe da, der funktionierte richtig. Und dann bemerkte ich den wesentlichen Unterschied. Der ESP32 auf dem ersten Versuchsboard hatte einen MicroPython-Kernel 1.18 und alle anderen hatten das letzte Release 1.19. Nachdem ich auch den zweiten ESP32 mit der Version 1.18 geflasht hatte, schnurrte der auch wie ein zufriedenes Kätzchen.
Ich hatte so etwas Ähnliches schon einmal in Bezug auf die Verwendung von PWM. Ich verstehe nicht, warum den MicroPython-Entwicklern solche Bugs passieren. Da funktioniert ein Feature perfekt und beim Updaten mit der Folgeversion kriegste graue Haare, weil nix mehr geht. Dabei sollten in der neuen Version Fehler beseitigt sein und nicht neue eingebaut.
Sicher, es kann schon auch einmal vorkommen, dass ein Bauteil defekt ist, gar so ein komplexes wie ein Controller. Wenn beim Experimentieren oder beim Nachbau einer Schaltung etwas nicht so funktioniert wie es sollte, dann suchen Sie nicht nur nach Fehlern im Programm oder in der Schaltung, sondern denken Sie auch daran, dass das Betriebssystem eines ESP, der Kernel, buggy sein kann. Deshalb habe ich oben schon darauf hingewiesen, dass Sie unbedingt die 1.18 verwenden müssen, damit dieses Projekt funktioniert.
Abbildung 4: DF-Player mini mit 4 GB SD-Karte
Der MP3-Player wird über eine RS232-Leitung angesprochen. Ich hatte früher schon einmal ein MicroPython-Modul dafür geschrieben. Weil das auf einem ESP8266 lief, hatte ich mir damals die Mühe gespart, Routinen für den Datenempfang aufzunehmen, der Kleine hat ja, wie ich schon bemerkt habe, dafür keine Leitung frei, die wird boardintern gebraucht als SD-D1 zum externen Flash-Chip. Für den ESP32 habe ich das Modul deshalb erweitert. Man kann jetzt zum Beispiel auch die Version, die Anzahl von Dateien auf der SD-Karte oder den Lautstärkepegel abfragen.
Die Kommunikation mit dem DF-Player geschieht stets in der gleichen Weise. Es werden Blöcke von 10 Bytes gesendet und empfangen. Der Aufbau eines solchen Blocks sieht wie folgt aus. Die Methode sendCommand() kümmert sich um die korrekte Übermittlung.
Startbyte 0x7E
Versionsnummer 0xFF
Länge der Payload 0x06 (von Versionsnummer bis Parameter 2 inkl.)
Commandbyte
Feedback 0x00 (nein) oder 0x01 (ja)
Parameter 1
Parameter 2
Checksum high
Checksum low
Endebyte 0xEF
Die Befehlscodes kann man im Datenblatt finden. Dieses habe ich benutzt, um die wesentlichsten Codes in Methoden des Moduls umzusetzen. Wir brauchen für das aktuelle Projekt nur ein paar davon. Werfen Sie aber ruhig mal einen Blick in die Datei dfplayer.py.
Damit eine Sprachausgabe möglich wird, brauchen wir die Ziffern und Zahlen als einzelne, kurze Sounddateien. Die Texte dafür habe ich mit dem Handy in einem Stück aufgenommen und via Bluetooth auf den PC übertragen, wo ich dann die Datei mit Audacity in die Sprachclips aufgeteilt habe. Audacity ist ein Freewaretool zum Bearbeiten von Audiodateien. Als Alternative können Sie auch Sprachgeneratoren verwenden, wie es in der Beitragsreihe Der sprechende Farbdetektor mit DFPlayer und TCS3200 verwendet wurde.
Um eigene Sprachbausteine herzustellen, nehmen Sie folgende Texte auf. Die Zahlen in der zweiten Spalte sind die Nummern, die später als Dateinamen der Clips beim Speichern auf der SD-Karte gebraucht werden.
ein 000
eins 001
zwei 002
…
zehn 010
elf 011
zwölf 012
dreizehn 013
…
neunzehn 019
zwanzig 020
dreißig 030
…
neunzig 090
einhundert 100
hundert 101
und 102
Die Datei vom Handy hat wahrscheinlich die Endung m4a. Damit kann Audacity leider nichts anfangen. Wir müssen eine Typumwandlung nach mp3 vornehmen. Das geht prima mit dem Online-Tool Convertio. Folgen Sie dem Link und ziehen Sie dann Ihre m4a-Datei auf das rote Feld.
Abbildung 5: convertio Startfenster © convertio.co
Im nächsten Fenster klicken Sie auf Konvertieren. Die Datei wird zum Server hochgeladen und …
Abbildung 6: convertio - Konvertieren © convertio.co
nach ein paar Sekunden können Sie Ihre mp3-Datei herunterladen.
Abbildung 7: convertio - Download © convertio.co
Starten Sie jetzt Audacity (oder alternativ einen Audioeditor Ihrer Wahl) und ziehen Sie die Datei ins Bearbeitungsfenster.
Abbildung 8: Audacity mit Sound-Datei
Mit der Strg-Taste und dem Mausrad zoomen Sie in die Spur hinein und heraus. Markieren Sie nun einen Clip durch Ziehen mit der Maus.
Mit Strg + Shift + A starten Sie den Speichervorgang der Auswahl. Geben Sie als Dateinamen die Nummern aus der obigen Liste ein. Sie können auch eine Textergänzung hinten anfügen, etwa 007-sieben. Klicken Sie auf Speichern. Danach dreimal Enter, weiter geht's zum nächsten Clip.
Abbildung 9: Audacity - Clip speichern
Kopieren Sie nun die Clips auf eine mini SD-Karte in den dort angelegten Ordner 00.
Vier externe Module müssen auf den ESP32 hochgeladen werden: VL53L0X.py, dfplayer.py, oled.py und ssd1306.py. Dann starten wir mit den Importen.
# bodysize.py
import VL53L0X
from dfplayer import DFPlayer
from machine import SoftI2C,Pin
from oled import OLED
from time import sleep
import sys
tof = VL53L0X.VL53L0X(i2c)
Wir instanziieren ein I2C-Objekt, das wir gleich an den Konstruktor der OLED-Klasse und der Klasse VL53L0X weiterreichen.
Das VL53L0X-Objekt wird initialisiert und gestartet. Der erste Wert wird eingelesen und eingestampft, das heißt wir machen damit nichts. In height legen wir die Höhe in Zentimeter ab, in der später der Sensor angebracht wird, zum Beispiel oben am Türrahmen.
tof.set_Vcsel_pulse_period(tof.vcsel_period_type[0], 18)
tof.set_Vcsel_pulse_period(tof.vcsel_period_type[1], 14)
tof.start()
size=tof.read()
print("ToF started")
height=200
Dann ist der DF-Player dran. Die Pin-Nummern der seriellen Leitungen RXD und TXD sowie der Bussy-Leitung werden deklariert und an den Konstruktor weitergegeben. Wir warten zwei Sekunden, bis der Player initialisiert hat. Die Verbindung klappt, wenn der Player uns die Anzahl der Titel auf der Karte mit 31 meldet.
RX_Pin = 18
TX_Pin = 19
bussyPin = 17
df=DFPlayer(bussyPinNbr=bussyPin, txd=TX_Pin, rxd=RX_Pin, volume=80)
sleep(2)
numOfTitles=df.getNumberOfFiles()
print("Anzahl Titel:",numOfTitles)
Ein Pin-Objekt für die Flash-Taste am ESP32 wird deklariert, dann definieren wir zwei Funktionen, die die eigentliche Arbeit erledigen, sprechen und die Körpergröße ermitteln.
def say(n):
assert n <= 199, "Der Wert muss zwischen 1 und 199 liegen"
h=n // 100
rest=n % 100
z=rest // 10
e=n % 10
print(h,z,e)
s=[]
Die auszusprechende Zahl muss zwischen 1 und 199 liegen. Das sichern wir mit assert ab. Wurde ein falscher Wert in n übergeben, wird eine AssertationError-Exception geworfen.
Dann spalten wir die Zahl in die Hunderter, den Zehnerrest, die Zehnerziffer und die Einerziffer auf. Eine leere Liste s wird angelegt und gleich mit den Nummern der abzuspielenden Soundclips gefüllt.
if h == 1:
s.append(100)
Auf die Anweisung df.play(0,100) spielt der DF-Player die Datei 100-einhundert.mp3 im Ordner 00 ab. Das tut er, wenn die Hunderterziffer 1 ist.
if 10 <= rest < 20:
s.append(rest)
Eine Sonderbehandlung brauchen die Zahlen zwischen 10 inklusive und 20 exklusive. Der Zehnerrest ist Namensbestandteil der entsprechenden Dateien und wird daher an die Soundliste s angehängt.
Weitere Sonderfälle sind die ganzen Zehner, zehn, zwanzig und so weiter. Hier ist der Zehnerrest größer oder gleich 20 und die Einerziffer ist 0. Alle weiteren Zehnerreste folgen der Regel Einerziffer "und" Zehnerziffer wobei im Falle einer 1 als Einerziffer zum Beispiel nicht "eins" "und" "dreißig", sondern "ein" " und" "dreißig" herauskommen muss.
elif rest >= 20:
if e == 0:
s.append(10*z)
else:
e= e if e != 1 else 0
s.append(e)
s.append(102)
s.append(10*z)
Der else-Zweig behandelt die Fälle 101 bis 109 und 1 bis 9. Zur Kontrolle lassen wir uns die Liste s im Terminal ausgeben. Diese Anweisung kann später wegfallen.
else:
s.append(e)
print(s)
Die for-Schleife liest uns jetzt die einzelnen Bestandteile der Liste s vor. Wenn der Player nicht gerade beim Abspielen einer Datei ist, fängt er damit an, den Track t im Ordner 00 darzubieten. Dann wartet der ESP32, bis der Player damit fertig ist. Das ist der Fall, wenn die Bussy-Leitung auf 0 geht. Die Methode isPlaying() überprüft das.
for t in s:
if not df.isPlaying():
df.play(0,t)
while df.isPlaying():
pass
return s
Die Funktion getSize() bestimmt die Körpergröße der Person, die unter dem Sensor durchgeht. Die Montagehöhe height desselben muss bekannt sein. Der Sensor erfasst den Abstand distance zwischen ihm und dem Kopf der Person. Die Körpergröße size ergibt sich dann aus der Differenz aus height und distance. Für genauere Ergebnisse führen wir mehrere Einzelmessungen durch, deren Anzahl wir im Parameter n übergeben. Der VL53L0X liefert seine Messwerte in Millimetern. Weil wir Zentimeter bevorzugen, teilen wir bei der Berechnung des Mittelwerts zusätzlich durch 10. Die Werte gehen in die Anzeige. Der auf eine Ganzzahl aufgerundete Wert der Körpergröße wird zurückgegeben.
Vor dem Eintritt in die Hauptschleife stellen wir die Lautstärke des Players auf 80%.
df.volume(80)
while 1:
size=getSize(10)
if size >= 50:
try:
say(size)
except AssertationError as e:
d.clearAll()
d.writeAt(" ERROR",4,0,False)
d.writeAt(" RANGE",4,1,False)
d.writeAt("OVERRUN",4,2)
if taste.value()==0:
d.clearAll()
d.writeAt("BODY SIZE",2,0,False)
d.writeAt(" PROGRAM ",2,1,False)
d.writeAt("TERMINATED",2,2)
sys.exit()
Wir fordern die Körpergröße mit zehn Einzelmessungen an. Damit der ESP32 nicht jedes Mal Laut gibt, wenn die Miezekatze den Sensor passiert, soll der Player nur plappern, wenn das Objekt größer als 50 cm ist. Sollten von getSize() fehlerhafte Werte über 199 geliefert werden, fangen wir die Exception mit try ab und geben eine Fehlermeldung im Display aus. Sonst wird der Wert abgespielt.
Um während der Entwicklung geordnet aus dem Programm auszusteigen, kann man die Flash-Taste am ESP32 drücken. Die Module sind dann noch aktiv. Sie können also händisch noch einige Experimente durchführen. Entdecken Sie zum Beispiel die Methoden des DF-Player-Moduls, oder nehmen Sie einen Begrüßungstext zusätzlich zu den Zahlen und Ziffern auf, den Sie beim Programmstart vorlesen lassen. Ein besonderer Gag ist es, Sensor- und Ansageteil der Schaltung zu trennen, dann hängen am Türstock keine Lautsprecher rum. Ein ESP32 erledigt den Messjob, ein zweiter die Ansage. Die Übertragung des Messwerts kann per WLAN über UDP erfolgen. Im weiteren Ausbau könnte eine Messwertanzeige mit einem großen Display aus 8x8_LED-Anzeigen entstehen. Denkbar wäre auch ein integrierter Passantenzähler…
Viel Spaß beim Basteln und Programmieren!
]]>Was das AZ-Touch Mod genau machen soll und wie es konfiguriert wird, erkläre ich Ihnen in diesem Beitrag.
Für diesen Blog ist das AZ-Touch MOD Wandgehäuseset ein Muss, siehe Tabelle 1.
Pos |
Anzahl |
Bauteil |
Link |
1 |
1 |
AZ-Touch MOD Wandgehäuseset |
|
2 |
1 |
Optional ESP32 NodeMCU Module WLAN WiFi Development Board |
|
3 |
1 |
Spannungsversorgung 12 V |
|
Tabelle 1: Hardware für diesen Blog
Zudem ist zwingend meine Modifikation am AZ-Touch nötig, da wir einen Zugriff auf den SD-Kartenslot des Touchdisplays benötigen und ggf. der Reset-Button an der Außenseite helfen könnte.
Nachdem ich mit openHAB angefangen hatte, wollte ich in meinen vier Wänden nicht immer nur über Tablet oder Smartphone auf die entsprechende Basisseite zugreifen. Gerade mit Kindern und deren Kuschelecke wollte ich ein einfaches Interface schaffen, worüber Sie z.B. einfach die Lichterkette ein- bzw. ausschalten können, oder aber andere Funktionen einfach bedienen können. Gerade für Kinder muss das entsprechende Device und die Bedienung so einfach wie möglich sein und ein einfaches Feedback zurückgeben, das die Kinder auch verstehen. Natürlich darf das Ganze nicht so verspielt sein, dass man als Erwachsener das Gefühl hat, ein Kinderspielzeug zu bedienen. Nach einer längeren Suche im Internet und das daraus resultierende Brainstorming, kam die Idee auf, das AZ-Touch Mod Wandgehäuse mit einem ESP32 zu verwenden. Im Internet bin ich durch Zufall auf einen älteren Artikel gestoßen, der mittels openHAB-API alles modelliert hatte, jedoch wurde dieses Projekt nicht weiter gepflegt und war nicht mehr lauffähig.
Noch vor dem Projekt kam dann aber die zweite Idee dazu, das AZ-Touch Mod Wandgehäuse so zu programmieren, dass keine APIs verwendet werden müssen, sondern auf generelles MQTT zurückgegriffen wird. Grund dieser Idee ist, dass auch ein Betrieb ohne Heimautomationsserver möglich sein soll, was über MQTT leicht realisierbar ist. Ein einfacher MQTT-Broker und IoT-Geräte, welche meist MQTT beherrschen, würden damit vollkommen ausreichen. Man könnte an der Stelle auch von einer Low-Budget-Heimautomationslösung reden.
Ein nicht zu unterschätzender Punkt war am Ende die Konfiguration der einzelnen Elemente auf dem Display. Schnell gibt es ein neues Gerät im Haushalt, das angesteuert werden soll und muss ich dafür wieder den ganzen Quellcode anfassen? Das wollte ich direkt vermeiden, weswegen zusätzlich zur Montage des AZ-Touch Mod Wandgehäuses noch eine Modifikation nötig ist, um auf den SD-Kartenslot zuzugreifen. Hier kann, mittels eines entsprechendem SD-Kartenadapters eine MicroSD-Karte verwendet werden, auf der die komplette Grund- und MQTT-Konfiguration gesichert ist. Wird also ein neues Element auf dem Display benötigt, muss nur die MQTT-Konfiguration erweitert werden. Ein Nachteil, den ggf. einige hier sehen könnten ist, dass natürlich die Zugangsdaten fürs WLAN auf der SD-Karte im Klartext einsehbar sind. Dazu muss man aber wissen, dass eine SD in dem AZ Wandmod verbaut ist.
Wie Sie sehen, steckt hinter dem anfänglichen Grundgedanken ein langer Findungsprozess. Gleichzeitig ist die Featureliste noch während der eigentlichen Arbeit stetig gewachsen, weswegen die Konfigurationsdatei schon Einträge hat, die es noch nicht in den aktuellen Release des Projektes geschafft haben.
Wenn ich Sie nun neugierig auf das Projekt gemacht habe, dann will ich zunächst ein bisschen auf die Oberfläche eingehen, siehe Abbildung 1.
Abbildung 1: Bedienoberfläche des AZ-Touch
Fangen wir mit dem oberen Rahmen an. Zunächst kann man eine Uhr einblenden, um sich die aktuelle Zeit anzeigen zu lassen. Wird diese nicht gewünscht, so wird der NTP-Server nicht eingerichtet und abgefragt, was sich auf die Funktionalität nicht auswirkt. Um ggf. schnell das Gerät zu finden, gibt es die Möglichkeit, dem Device einen Namen zu geben. In der rechten Ecke wird die Signalqualität des WiFi angezeigt. Somit hat man erst einmal die wesentlichen Informationen am oberen Rand.
Herzstück sind die sechs Buttons, die nicht zu übersehen sind. Diese dienen entweder für die reine Informationsanzeige, in meinem Fall 28 Grad bei 40% Luftfeuchtigkeit und einem Button, mit dem MQTT-Stecker ein- bzw. ausgeschaltet werden. Elemente, wie z.B. dem Schalter-Button, geben ein optisches Feedback, ob der Schalter ein- oder ausgeschaltet sein sollte. Hier wird das Feedback von MQTT genutzt, sprich der übermittelte Status des Devices via MQTT.
Weitere Button-Modi sind geplant, bisher sind nur die Modi Info, Measurement, Light und Switch wirklich nutzbar, dazu aber später mehr.
Wie vllt. zu erkennen, sind aktuell nur diese sechs Button möglich, was sich aber mit kommenden Projektständen ändern soll. Hier ist geplant, dass über den Button unten rechts durch das Menü „gescrollt“ werden kann.
In dem Bild nicht zu sehen, aber doch vorhanden, sind Anzeigen von Fehlermeldungen. Wenn z.B. der MQTT-Broker nicht mehr erreichbar ist, oder das WiFi Probleme macht, wird dies über einen entsprechend Dialog auf der Oberfläche angezeigt. Damit ist aber auch gleichzeitig die normale Bedienung über die Touchoberfläche nicht mehr möglich, bis der Fehler behoben wurde.
Ein weiteres Kernfeature, welches auch in meinen Jobs teilweise bewusst missachtet wurde, ist der Watchdog. Sollte der Fall eintreten, dass einmal der verbaute Mikrocontroller hängen bleibt, so wird das Device automatisch neu gestartet. Dieses Feature kann ein- bzw. ausgeschaltet werden und auch der Timeout in Sekunden festgelegt werden, dazu aber mehr bei der Erklärung der Konfiguration.
Ein weiteres Feature ist die Uhr, die angezeigt wird, siehe Abbildung 2. Diese wird eingeblendet, wenn die Zeit angezeigt werden soll und damit der NTP-Dienst startet.
Abbildung 2: Aktuelle Uhrzeit und Datum
Damit wird nicht immer die Statusseite aller Geräte gezeigt. Ist dies doch einmal notwendig, so reicht eine Berührung des Touchdisplays, um auf die Buttonseite zu gelangen. Auch hier wird der Wechsel von Button-Oberfläche zur Uhr über eine einstellbare Zeit in der Konfiguration realisiert.
Wie bei der Vorstellung des Displays erwähnt, wird über Konfigurationsfiles auf einer SD-Karte das AZ Wandmod konfiguriert. Dazu wird bei Start versucht, auf den SD-Kartenslot des Displays zuzugreifen. War dies erfolgreich, so wird nach folgenden Dateien gesucht, die zwingend auf der SD-Karte liegen müssen:
Wie die Endung vllt. vermuten lässt, handelt es sich um zwei separate XML-Dateien. Die Syntax von xml ist nicht schwierig, ich empfehle an der Stelle gerne Notepadd++ mit dem Plugin XML Tools, welches direkt auf Fehler in der XML-Syntax beim Speichern hinweist.
Zunächst die general.xml, welche die Grundkonfiguration fürs AZ Wandmod beinhaltet. An dieser Stelle wird z.B. der Zugang zum WiFi und dem MQTT-Broker beschrieben. Schaut man sich die Beispieldatei im Projekt an, siehe Code 1, ist die Konfiguration überschaubar.
<root>
<GUI name="AzTouch openHab" darkMode="0" showTime="1"/>
<WiFi name="WiFi" SSID="Test-WiFi" Pass="Test-Pass"/>
<Secure name="Secure" active="0" pass="12345" timeout="5"/>
<MQTT name="MQTT" broker="Testbroker" port="1883" user="" pass="" clientName="Wallmount"/>
<Watchdog name="Watchdog" active="1" time="10"/>
</root>
Code 1: Beispieldatei von general.xml
Tabelle 2 erklärt dabei jeden einzelnen Parameter, um Missverständnisse auszuräumen.
Knoten |
Attribut |
Beschreibung |
Default |
GUI |
name |
Name der auf der Oberfläche angezeigt wird. Max 18. Zeichen auf dem Display erlaubt. |
AzTouch openHab |
|
darkMode |
Wird in einem späteren Release zwischen einem hellen oder dunklen Bildschirm wechseln |
„0“, helles Display |
|
showTime |
Soll die Uhrzeit angezeigt werden? Startet einen NTP-Client und synct die Zeit |
1 |
WIFI |
name |
Name des Knotens, hat hier keine Bedeutung |
„WiFi“ |
|
SSID |
Hier die SSID des WLANs eintragen |
„Test WiFi“ |
|
Pass |
Passwort des WLANs |
„Test-Pass“ |
Secure |
name |
Name des Knotens, hat in der Konfiguration keine Bedeutung |
„Secure“ |
|
active |
(De-)aktiviert das Numpad auf dem Display, um das Passwort einzugeben. Noch nicht implementiert!!! |
„0“ |
|
pass |
Das Passwort zum Freischalten, nur Zahlen erlaubt |
„12345“ |
|
timeout |
Die Zeit, die vergehen muss, bis der Bildschirm wieder gesperrt wird und die Uhr oder das Numpad für die Zahleneingabe. Zeit in Minuten |
5 |
MQTT |
name |
Name des Knotens, hat in der Konfiguration keine Bedeutung |
„MQTT“ |
|
broker |
Hier den DNS-Namen oder die IP-Adresse eintragen |
„Testbroker“ |
|
port |
Der Port zu MQTT, kann abweichen |
1883 |
|
user |
Sofern ein User zur Anmeldung gebraucht wird, hier den User eintragen |
„“ |
|
pass |
Passwort für den User, um sich mit MQTT zu verbinden |
„“ |
|
clientName |
Beschreibt den Namen, unter dem sich das Device bei MQTT anmeldet. Keine Leerzeichen im Namen erlaubt! |
„Wallmount“ |
Watchdog |
name |
Name des Knotens, hat in der Konfiguration keine Bedeutung |
„Watchdog“ |
|
active |
(De-)aktiviert den Watchdog |
„1“ |
|
time |
Die Watchdogzeit in Sekunden. Achtung: Werten unter 2 Sekunden können zu einem boot-loop führen |
„10“ |
Tabelle 2: Erläuterung der Parameter general.xml
Mehr Einträge sind in dieser Datei nicht vorhanden und werden auch nicht geprüft. Um Fehler oder Probleme zu vermeiden, sollte die Beispieldatei als Vorlage verwendet werden.
Fast genauso wichtig wie die general.xml ist die MQTT.xml. Diese beschreibt aktuelle mehrere Dinge:
Auch an der Stelle hilft das Beispiel aus dem Projekt, siehe Code 2.
<root>
<Element name="Switch" type="Switch" pos="1" page="1">
<pub>cmnd/NOUS_Monitor/POWER</pub>
<sub>stat/NOUS_Monitor/POWER</sub>
</Element>
<Element name="Info" type="Measurement" pos="2" page="1">
<sub unit="Grad">shellies/shellyht-office/sensor/temperature</sub>
<sub unit="%">shellies/shellyht-office/sensor/humidity</sub>
</Element>
</root>
Code 2: Beispieldatei für MQTT.xml
Für das Verständnis der einzelnen Knoten und Attribute hilft Tabelle 3.
Knoten |
Attribut |
Beschreibung |
Default |
Element |
name |
Name für den Button |
„“ |
|
type |
Art des Buttons, wird weiter unten genauer beschrieben |
„“ |
|
pos |
Gibt die Position auf der jeweiligen Seite an |
„-1“ |
|
page |
Gibt an, auf welcher Seite der Button sein soll |
„-1“ |
Element -> pub |
|
Beschreibt, an welchen MQTT-Knoten ein Kommando gesendet werden soll |
|
Element -> sub |
|
Beschreibt, welcher Knoten für den Status überwacht werden soll |
|
|
Unit |
Spezialfall beim type „Measurement“ oder „Info“, hier wird ein Suffix an den Inhalt der Nachricht angefügt |
„“ |
Tabelle 3: Erläuterung der Parameter MQTT.xml
Bei der Definition gibt es aber ein paar Regeln. Zwar kann man beliebig Position und Seite für den Button vergeben, jedoch überprüft das Programm intern, wie viele Seiten tatsächlich benötigt werden und ob die Position schon belegt ist. Wird eine Seite ausgewählt, die nicht den kalkulierten Seiten entspricht, oder aber die Position auf der Seite schon belegt sein sollte, so werden diese Elemente erst einmal übersprungen und nachträglich Lücken in den Seiten gefüllt. Sollten zu wenige Seiten vorhanden sein, so wird auch hier erweitert, sodass alle Elemente Platz finden.
Bei den Typen „Measurement“ oder „Info“ ist es möglich, bis zu drei Sub-Knoten zu hinterlegen. Diese dienen aber nur der Anzeige und können nichts an den Broker verschicken! Dadurch werden dann in dem Button in drei Zeilen die Werte angezeigt. Das ist z.B. praktisch, wenn man hier Umgebungswerte anzeigen möchte.
Eine große Frage wird wahrscheinlich noch sein, welche Typen es alles gibt, dazu gibt Tabelle 4.
Type |
Beschreibung |
Bild |
Bemerkung |
Info |
Zeigt bis zu drei Werte an, entsprechende Sub-Knoten müssen dem Element angehängt sein |
Symbol ist grau beim Button hinterlegt |
|
Measurement |
Zeigt bis zu drei Messwerte an. Entsprechende Sub-Knoten werden benötigt |
Symbol ist grau beim Button hinterlegt |
|
Light |
Button um einen Lichtschalter zu symbolisieren |
Wenn „off“ rot, sonst grün |
|
Socket |
Button, der z.B. für eine Steckdose gedacht ist |
Wenn „off“ rot, sonst grün |
|
Switch |
Button, der ein Ein-/Aus-Schalter symbolisiert |
Wenn „off“ rot, sonst grün |
|
Shutter |
Knoten definiert einen Rollladen, der jeweils zwei Sub und Pub-Knoten benötigt. Dadurch werden zwei getrennte Buttons, die untereinander dargestellt werden, angezeigt |
|
Ist aktuell noch nicht in der Software implementiert. |
|
Symbol auf dem Display, um zwischen den Seiten zu „scrollen“ |
Kann nicht als Typ in der Konfiguration ausgewählt werden. Ist immer Schwarz |
Tabelle 4: Aktuell vorhandene Typen
Das Projekt ist bei mir schon seit gut 5 Monaten am Laufen. Während der Entwicklung sind viele Ideen entstanden und auch viele Ideen wieder verworfen worden. Letztlich haben es folgende Features in meine Release 1.2.2 geschafft:
Damit ist das Ende meiner Featureliste zwar nicht erreicht, es reicht aber zunächst für den ersten Start. Da ich aktuell keine elektrischen Fensterheber habe, hat das Feature „Shutter“ noch keine große Relevanz für mich, was sich aber bestimmt noch ändern wird.
Dieses Projekt ist parallel zu meiner Blogserie über openHAB entstanden. Dadurch habe ich noch einmal viel über openHAB, aber auch MQTT gelernt. Das Projekt kommt aber auch sehr gut ohne openHAB aus! Lediglich ein MQTT-Broker und ein paar IoT-Geräte sind notwendig, um mit dem Display schon etwas machen zu können. Gerade mein 3D-Drucker oder mein großer Arbeits-PC haben Tasmota-Smart-Steckdosen verpasst bekommen, um den Stromverbrauch im Haus zu senken. Denkbar sind aber auch andere Konzepte in den eigenen vier Wänden. Da MQTT eines der Protokolle ist, die fast jedes gängige Smart-Home-Device kennt,
Das hier vorgestellte Projekt ist noch lange nicht fertig. Schon vor Release ist der Quellcode auf meinem GitHub-Repository verfügbar, aber eben noch nicht so detailliert beschrieben. Viele Features und Bugfixes warten noch bei diesem Projekt auf mich, aber gerade solche Projekte zeigen, dass es nicht immer eine teure Lösung von namenhaften Herstellern braucht, um z.B. die Heimautomation von openHAB auf ein externes Display zu bringen. Mit dem bisherig verwendeten Arbeitsspeicher des NodeMCU ist noch viel möglich. Das komplette Projekt ist zu finden unter https://github.com/M3taKn1ght/AZ-Touch-Control
]]>LCD- und OLED-Displays sind schön. Es lassen sich einige Informationen auf den 2, 3 oder 6 Zeilen darstellen. OLEDs sind sogar grafikfähig. Aber manchmal wünsche ich mir eine Anzeige mit wirklich großen Ziffern. Bei meinen kleinen Selbstbau-Waagen wie in Teil 1 mit 100g und 1000g habe ich OLED-Displays eingesetzt. Nun kam ein 20kg-Exemplar dazu und da fand ich die 0,96"-Anzeige, aber auch eine übliche 1602-LCD denn doch zu popelig.
Beim Suchen im Netz stieß ich dann auf eine 6-fach-LED-Anzeige mit 14mm hohen Ziffern. Das war es genau, was ich brauchte, zumal die Ansteuerung nur über zwei Leitungen erfolgt. Mit Hilfe des Datenblatts stellte sich schnell heraus, dass das Übertragungsprotokoll ziemlich genau dem I2C-Protokoll entspricht, aber eben nicht ganz. Die einzige Abweichung: es wird keine Hardwareadresse zu Beginn des Transfers gesendet. Aber sonst gibt es eine Start-Condition, eine Stop-Condition und ein Acknowledge-Bit, wie beim I2C-Bus.
Natürlich kann durch diese Umstände das, im Kernel von MicroPython eingebaute, I2C-Modul leider nicht verwendet werden. Also habe ich ein Ersatzmodul auf der Basis des Datenblatts gestrickt, das optimal die Bedingungen für das Display der Waage erfüllt. Eine Überraschung hatte das Display dennoch auf Lager. Doch dazu später mehr. Wie man das Display dazu bringt Klartext-Zahlen rechtsbündig darzustellen, lesen Sie in dieser Folge von
heute
Kümmern wir uns zuerst einmal um die Hardware des Displays. Außer diesem selbst wird neben den bisherigen Baugruppen für die Waage nichts weiter benötigt. Der Treiberbaustein für die Sieben-Segment-Anzeigen sitzt auf der Unterseite des Moduls.
Abbildung 1: TM1637 von oben
Abbildung 2: TM1637 von unten
Das bisherige Drumherum sind der ADC für die Waage, ein HX711-Modul und ein Controller vom Typ ESP8266 oder ESP32, sowie eine Taste, um die Tara zu berücksichtigen. Wie der Controller mit dem Treiberbaustein zusammenarbeitet, das erfahren Sie im ersten Post zum Thema Waage. Wir werden hier das Modul für die neue Anzeige durchleuchten und das Betriebsprogramm der Waage auf das neue Display anpassen.
1 |
D1 Mini NodeMcu mit ESP8266-12F WLAN Modul oder D1 Mini V3 NodeMCU mit ESP8266-12F oder NodeMCU Lua Amica Modul V2 ESP8266 ESP-12F WIFI oder NodeMCU Lua Lolin V3 Module ESP8266 ESP-12F WIFI oder ESP32 Dev Kit C unverlötet oder ESP32 Dev Kit C V4 unverlötet oder ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder NodeMCU-ESP-32S-Kit oder |
1 |
TM1637 6 Digit blaue LED-Anzeige 7 Segment Display Modul mit 0,56 Zoll |
1 |
Wägezelle 20 kg |
1 |
HX711 AD-Wandler für Wägezellen |
1 |
|
diverse |
Jumper Wire Kabel 3 x 40 STK. je 20 cm M2M/ F2M / F2F evtl. auch |
optional |
Der Logic Analyzer ist ein sehr nützliches Instrument, wenn es bei der seriellen Datenübertragung hakt. Er ersetzt in vielen Fällen ein teures DSO (Digitales Speicher Oszilloskop) und bietet darüber hinaus noch den Vorteil längerer Aufzeichnungen, in die man dann gezielt hineinzoomen kann. Zu dem hier verlinkten Gerät gibt es eine kostenlose Betriebs-Software, das Teil wird über den PC angesteuert. Mir hat es schon in vielen verzweifelten Fällen geholfen, auch in diesem Fall. Das Protokoll des TM1637 ist zwar im Datenblatt ausreichend dargestellt, doch übersieht man schon gerne mal ein Detail. Vergleicht man dann das Impulsdiagramm im Datenblatt mit dem selbst erstellten, kommt man sehr schnell auf die Lösung des Problems.
Hier sind die Schaltungen für ESP32 und ESP8266:
Abbildung 3: Waage - Schaltung für ESP32 und ESP8266
Abbildung 4: Anzeige Aufbau mit TM1637 im Test mit dem ESP8266 D1 mini
Thonny oder
hx711neu.py API für den AX711
scale1637.py Das Betriebsprogramm
tm1637.py API für die 7-Segment-Anzeige
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 18.06.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Der TM1637 verwendet keine Hardwareadresse, wie es normalerweise auf dem I2C-Bus üblich ist, das habe ich oben schon erwähnt. Es gibt auch keine Register, sondern nur Kommandos, Commands, nämlich drei: Data command, Display and control command und Address command. Die Signalfolge in der folgenden Abbildung stellt den Schreibzugriff mit automatischem Hochzählen der Adresse nach jedem gesendeten Daten-Byte dar.
Die Sequenz beginnt mit einer Start-Condition, DIO geht auf LOW, während CLK auf HIGH ist.
Abbildung 5: Signalverlauf beim Schreiben ins SRAM des TM1637
Mit der fallenden Taktflanke stellt der Controller das erste Datenbit auf die DIO-Leitung und setzt daraufhin CLK auf HIGH, der TM1637 übernimmt das Bit. Das erste Byte ist das Data command, 0x40. Sind 8 Bits, beginnend beim LSB (Least Significant Bit = niederwertigstes Bit), übertragen, zieht der TM1637 mit fallender Taktflanke DIO auf LOW, wenn die Übertragung OK war. Die neunte steigende Taktflanke triggert das Acknowledge-Bit. Es folgt eine Stop-Condition (CLK ist HIGH, DIO zieht nach einer Verzögerung nach) und sofort danach eine erneute Start-Condition.
Danach sendet der Controller mit dem Address command 0xC0 die erste Speicheradresse, ab welcher die Daten fortfolgend abgelegt werden. Nach jedem Daten-Byte kommt ein Acknowledge und nach dem letzten Byte eine Stop-Condition.
Das dritte Kommando, mit eigener Start-Condition, Acknowledge und Stop-Condition, steuert das Display. Die unteren drei Bits setzen die Helligkeit, Bit 3 schaltet die Anzeige an oder aus.
Schauen wir uns an, wie das alles programmtechnisch umgesetzt werden kann. Wir starten mit einem geringen Importaufkommen.
from machine import Pin
from time import sleep_us, sleep_ms
Es folgen ein paar Exception-Klassen für die Fehlerbehandlung. Die Container-Klasse TM1637_Error erbt von Exception, der Mutter aller Ausnahmeklassen. Die Subklassen erben von TM1637_Error.
class TM1637_Error(Exception):
pass
class BrightnessError(TM1637_Error):
def __init__(self):
super().__init__("Falscher Kontrastwert",
"0 <= Wert <= 7")
class PositionError(TM1637_Error):
def __init__(self):
super().__init__("Falscher Positionswert",
"0 <= Wert <= 5")
class StringLengthError(TM1637_Error):
def __init__(self):
super().__init__("String zu lang",
"0 <= Wert <= 5")
Die Klasse TM1637 wird deklariert. Die Konstanten setzen die Basiswerte für die Commands. MSB dient zum Aktivieren des Dezimalpunkts eines Digits, indem es zum Segmentcode oderiert wird.
class TM1637():
DataCmd = const(0x40) # data cmd - write, autoincr., normal
AdrCmd = const(0xC0) # address command f. Register 0
DispCntrl = const(0x80) # disp ctrl cmd - an/aus Kontrast
DispOn = const(0x08) # display an
MSB = const(0x80) # Dezimalpunkt
a=[2,1,0,5,4,3]
Segm=bytearray(b'\x3F\x06\x5B\x4F\x66\x6D\x7D\x07\x7F\x6F')
Zu den Variablen, der Liste a und dem Bytearray Segm muss ich etwas ausholen.
Die Abfolge der Digits im Display war zu meinem Erstaunen nicht von links nach rechts, oder meinetwegen auch umgekehrt, sondern so wie in Abbildung 3. Das verkompliziert die Sache ein wenig.
Abbildung 6: Displayanordnung
Wenn ich einen Anzeigestring aus einem Messwert bilde, können die Ziffern nicht in ihrer natürlichen Reihenfolge an das Display gesendet werden, weil das ein kleines Durcheinander erzeugt. Aus 123456 würde 321654, mal was Anderes! Die Liste a=[2,1,0,5,4,3] stellt die Zuordnung zwischen String und realer Anzeigeposition her. Was im String an der Position 0 steht, muss in den Speicher für das zweite Digit geschrieben werden, damit die Ziffer ganz links in der Anzeige auftaucht. Die 1 muss in Digit 2 landen. Der Index der Liste ist also die Position im Ziffernstring, das Listenelement, die Digitnummer, wo die Ziffer, oder besser, deren Segmentmuster, landen soll. Ich komme später noch einmal darauf zurück.
Das Bytearray Segm enthält die Segmentmuster der Ziffern 0 bis 9 nach dem Schema in Abbildung 5.
Abbildung 7: Segmentanordnung
Jedes Segment entspricht einer Bitposition nach folgendem Muster.
Abbildung 8: Zifferncodierung
Wenn wir 0x6D in den Anzeigespeicher 3 schreiben, erscheint eine 5 in der Position rechts außen im Display und wenn wir mit der Adresse 0xC0 beginnen, dann muss 0x6D als vierter Wert übertragen werden, um in 0xC3 zu landen.
Weiter geht es mit dem Konstruktor der Klasse TM1637, der Methode __init__().
def __init__(self, clk=Pin(5), dio=Pin(4), brightness=3):
self.clk = clk
self.dio = dio
if not 0 <= brightness <= 7:
raise BrightnessError
self.brightness = brightness
self.clk.init(Pin.OUT, value=1)
self.dio.init(Pin.OUT, value=1)
self.delay=5
sleep_us(10) # 10us warten
self.clearDisplay()
print("TM1637 ready")
Es können drei optionale Schlüsselwortparameter übergeben werden, die Pin-Objekte für CLK und DIO, sowie für den Kontrast oder auch die Helligkeit, wie Sie wollen. Wird kein Argument übergeben, dann gelten die Defaultwerte. Alle Parameter werden Attributen zugewiesen, der Kontrastwert wird über dies auf Einhaltung des Wertebereichs überprüft. Liegt brightness nicht im zulässigen Bereich, dann wird eine BrightnessError-Exception geworfen.
Die Pins werden auf Ausgang gesetzt. Als Verzögerung für den Takt lege ich 5µs vor, das entspricht einer Frequenz von 100kHz. Wir warten kurz, löschen das Display, dann meldet der Konstruktor die Einsatzbereitschaft des Objekts im Terminal.
Mit latency() können wir das ganzzahlige Argument in val als den Wert der Verzögerung im Attribut delay ablegen, nachdem der Wertebereich (1…20 für 500kHz…50kHz) gegebenenfalls eingegrenzt wurde. Ohne Argument aufgerufen, liefert die Methode den aktuellen Wert von delay zurück.
def latency(self, val=None):
if val is None:
return self.delay
else:
if type (val) != int:
raise LatencyTypeError
val = min(max(val,1),20)
self.delay=val
return val
Die Methode startCond() folgt den oben genannten Vorgaben für die Signalsequenz. Der Ruhezustand auf beiden Leitungen ist HIGH. DIO geht zuerst auf LOW, dann folgt CLK.
def startCond(self):
self.dio(0)
sleep_us(self.delay)
self.clk(0)
sleep_us(self.delay)
Für das Erzeugen einer Stop-Condition muss DIO zuerst sicher auf LOW sein und die Taktleitung auf HIGH. verzögert geht dann DIO auf HIGH.
def stopCond(self):
self.dio(0)
sleep_us(self.delay)
self.clk(1)
sleep_us(self.delay)
self.dio(1)
Zwischen Start- und Stop-Condition eingebettet ist der Transfer des Data-Command-Bytes.
def writeDataCmd(self):
self.startCond()
self.writeByte(DataCmd)
self.stopCond()
Das Nämliche gilt für writeDispCntrl(). Allerdings werden auf das nackte Kommandobyte 0x80 weitere Bits durch Oderieren aufgepfropft. Mit DispOn = 0x08 setzen wir Bit 3. Die drei Kontrastbits 2:0 stehen in brightness.
def writeDispCntrl(self):
self.startCond()
self.writeByte(DispCntrl | DispOn | self.brightness)
self.stopCond()
writeByte() ist die universelle Methode zum Versenden eines Bytes unter Berücksichtigung des Acknowledge-Bits, das aber nicht gescannt wird. Wir müssten sonst DIO auf Eingang schalten, den Zustand einlesen und anschließend wieder auf Ausgang schalten. Bislang ist kein Fehler aufgetreten, also habe ich die Prüfung weggelassen.
def writeByte(self, b):
for i in range(8):
self.dio((b >> i) & 1)
sleep_us(self.delay)
self.clk(1)
sleep_us(self.delay)
self.clk(0)
sleep_us(self.delay)
sleep_us(self.delay) # ACK-Takt folgt
self.clk(1)
sleep_us(self.delay)
self.clk(0) # naechstes Byte vorbereiten
sleep_us(self.delay)
Die for-Schleife schiebt das übergebene Byte mit dem LSB beginnend auf die DIO-Leitung. CLK ist von der Start-Condition her noch auf LOW. Das Byte wird um i=0 bis 7 Positionen nach rechts geschoben und jetzt das LSB maskiert. Das Ergebnis ist 0 oder 1. Damit wird der Ausgang gesteuert.
Nachdem der Zustand stabilisiert ist, erzeugen wir an CLK eine steigende Flanke, der TM1637 sampelt den Zustand auf DIO. Nachdem der Takt wieder auf LOW ist, folgt die Bereitstellung des nächsten Bits, der Vorgang wiederholt sich, bis alle Bits draußen sind. CLK bleibt nach dem letzten Bit für delay Sekunden auf LOW, dann folgt als letztes der Acknowledge-Takt, der wieder mit CLK=LOW endet. Es kann nun ein weiteres Byte oder eine Stop-Condition folgen.
Zum Testen der Anzeige aber auch zur Ausgabe ganz spezifischer Muster, zum Beispiel für ASCII-Zeichen, dient die Methode segment(). In seg wird das Muster übergeben (default 0xFF) und in pos die Nummer des Digits (default 0x00). Die Ausgabeposition wird überprüft.
def segment(self,seg=0xFF,pos=0):
if not 0 <= pos <= 5:
raise PositionError
self.writeDataCmd()
self.startCond()
self.writeByte(AdrCmd | TM1637.a[pos])
self.writeByte(seg)
self.stopCond()
self.writeDispCntrl()
writeDataCmd() hat eigene Start- und Stop-Conditionen. Bevor die Adresse gesendet wird, muss aber eine Start-Condition eingebaut werden. Nach der Basis- Speicheradresse mit oderierter Digitnummer folgen das Segmentbeschreibungs-Byte, die Stop-Condition und das display-Control-Byte. pos spricht die reale Position des Digits in der Anzeige an, das indizierte Listenelement die physikalische Speicheradresse, aus 0 wird so die 2, aus 5 die 3 etc.
>>> from tm1637 import TM1637
>>> tm=TM1637()
>>> aber=bytearray(b'\x77\x7C\x79\x50')
>>> for i in range(len(aber)):
tm.segment(aber[i],i)
Abbildung 9: Schriftzug AbEr
kontrast() funktioniert ähnlich wie latency(). Ohne Argument wird der aktuelle Wert zurückgegeben. Mit einem Wert zwischen 0 und 7 inklusive der Grenzen wird die Helligkeit neu eingestellt. Im Zusammenhang mit einem Fotowiderstand könnte man zum Beispiel so die Helligkeit der Anzeige dem Umgebungslicht anpassen.
def kontrast(self, val=None):
if val is None:
return self._brightness
if not 0 <= val <= 7:
raise BrightnessError
self.brightness = val
self.writeDataCmd()
self.writeDispCntrl()
Um das Display zu löschen sende ich sechs Null-Bytes.
def clearDisplay(self):
segments=(bytearray(b'\x00\x00\x00\x00\x00\x00'),-1)
self.writeSegments(segments)
Das Tupel segments enthält ein Bytearray mit den Segmentcodes und eine Ganzzahl. Diese gibt die Nummer des Digits an, bei dem der Dezimalpunkt angesteuert werden muss, falls es sich bei der Zahl um den Typ float handelt. Der Wert -1 deutet auf eine Ganzzahl hin. Wir kommen weiter unten noch genauer darauf zu sprechen. Das Tupel übergeben wir an writeSegments().
Einen Funktionstest aller Filamente erledigt lampTest() nach demselben Muster wie clearDisplay().
def lampTest(self):
segments=(bytearray(b'\xFF\xFF\xFF\xFF\xFF\xFF'),-1)
self.writeSegments(segments)
Bis zu sechs Segmentmuster ab einer vorgegebenen Position senden, das kann writeSegments(). Die Muster stehen im Tupel segmente, dahinter kommt die Position. Für diesen Wert führen wir eine Plausibilitätskontrolle durch.
Nun dröseln wir das Tupel in Muster und Dezimalpunkt-Position auf. Der String darf nur so lang sein, wie ab pos noch Digits dafür da sind, wir testen das.
Passt alles, schicken wir das Data-Command, gefolgt von einer Start-Condition und der Start-Adresse. Die for-Schleife bringt die Ziffern an die korrekte Position.
def writeSegments(self, segmente, pos=0):
if not 0 <= pos <= 5:
raise PositionError
s,p=segmente
# print(s,p)
if len(s) + pos > 6:
raise StringLengthError
self.writeDataCmd()
self.startCond()
self.writeByte(AdrCmd | pos)
for i in range(pos,6):
c=s[TM1637.a[i]]
if p==TM1637.a[i]:
c|=MSB
self.writeByte(c)
self.stopCond()
self.writeDispCntrl()
Die Segmentmuster für Zahlen, die wir mit number2Segments() erzeugen, beginnen alle ab der realen Digit-Position ganz links außen. Das ist die physikalische Position 2 im Speicher. Beginnen müssen wir die Sendesequenz aber mit der relativen Speicheradresse 0, absolut 0xC0, sonst müssten wir jedem Datenbyte die Adresse vorausschicken. Wir wollen aber das Autoincrement nutzen und die sechs Daten-Bytes in einem Abwasch senden. Auch hier hilft wieder die Liste a= [2,1,0,5,4,3]. Sie sagt uns nämlich, welches Zeichen des Strings an welche Speicherstelle gesendet werden muss.
Abbildung 10: String auf Speicher zuweisen
Das i in der for-Schleife durchläuft die physikalischen Speicherpositionen. Es dient als Zeiger in die Liste a. Das Element an dem jeweiligen Listenplatz ist ein Zeiger auf die Position des Zeichens im String beziehungsweise Bytearray. Der Code für dieses Zeichen wird in die Speicherstelle geschrieben, die gerade mit i adressiert wird.
Wenn p den Wert von a[i] hat, wird zu dem Segment-Code noch das MSB oderiert, was dazu führt, dass der Dezimalpunkt aktiviert wird. Dann wird das Byte zum TM1637 geschickt.
Nach den in der Regel sechs Bytes kommt eine Stop-Condition und danach der Display-Control-Command.
Fehlt noch die Codierung von Ganzzahlen und Fließkommazahlen in Segmentcodes. Number2Segments() nimmt die Zahl, die mit Komma und Vorzeichen natürlich nicht länger als 6 Zeichen sein darf und ein optionales Argument k. Mit diesem geben wir die Anzahl von Nachkommastellen an, falls die Zahl vom Typ float ist. Auf den Typ prüfen wir als Erstes.
def number2Segments(self, n, k=1):
if type(n)==int:
s="{:>6}".format(n)
elif type(n)==float:
s="{:>7."+str(k)+"f}"
s=s.format(n)
else:
raise TypeError
Ist der Typ int, also Ganzzahl, dann wandeln wird den Wert über den Formatstring in einen rechtsbündig formatierten (">") String, mit eventuell führenden Leerzeichen, von der minimalen Länge 6 um. Ist die Zahl vom Typ float, müssen wir berücksichtigen, dass bei der Nachbehandlung des Strings der Dezimalpunkt als separates Zeichen wegfällt. Deswegen geben wir dem String eine minimale Breite von 7 Zeichen. In den Formatstring arbeiten wir die Anzahl Nachkommastellen ein.
pos=s.find(".")
if pos != -1:
s=s.replace('.','')
pos-=1
Dann suchen wir nach der Position eines potenziellen Dezimalpunkts. Existiert keiner, dann ist es eine Ganzzahl, pos erhält den Wert -1. Andernfalls enthält pos den Index auf den Punkt. In diesem Fall ersetzen wir den Punkt im String durch ein leeres Zeichen. Die Position verringern wir um 1, denn der Punkt muss beim Digit davor berücksichtigt werden.
if len(s)>6:
raise StringLengthError
segments = bytearray(len(s))
Ist jetzt der aufbereitete String länger als 6 Zeichen, werfen wir eine StringLengthError-Exception. Ist alles im grünen Bereich, erzeugen wir ein Bytearray von der Länge des Strings. Der dürfte nach der momentanen Lage stets die Länge 6 haben. Jetzt geht es ans eigentliche Codieren. Die for-Schleife klappert jedes Zeichen ab.
for i in range(len(s)):
if s[i] == " ":
segments[i]=0x00
Ist das Zeichen an der Position i ein Leerzeichen, darf kein Filament leuchten – Segmentcode 0x00.
elif s[i] == "-":
segments[i]=0x40
Ist es ein Minuszeichen, dann brauchen wir nur den Mittelstrich – Code 0x40
else:
segments[i] = TM1637.Segm[ord(s[i]) - 48]
return (segments,pos)
In allen anderen Fällen holen wir den Code aus dem Bytearray Segm. Als Index dient uns der um 48 verringerte ASCII-Code der Ziffer. 48 ist der ASCII-Code der "0".
Zurückgegeben wird das Bytearray zusammen mit der Punktposition als Tupel.
Durch den Einsatz des LED-Displays ist das Betriebsprogramm deutlich schlanker geworden. Das liegt an der simpleren Art der Displayansteuerung. Im Zusammenhang mit der Waage habe ich dem Display auch ein wenig Klartext beigebracht, es kann "Error" und "tara", nicht gerade künstlerisch wertvoll, aber für den Zweck ausreichend. Mit den effektiv 40 Programmzeilen ist das Programm sehr übersichtlich. OK, die Hauptarbeit wird in den Modulen hx711 und tm1637 erledigt, aber selbst die sind mit 139 beziehungsweise 161 Zeilen noch recht schnuckelig. Auf jeden Fall passt alles ganz locker auch in einen ESP8266.
from machine import Pin
from time import sleep
from tm1637 import TM1637
from hx711neu import HX711
Wir brauchen Pins für die GPIO-Steuerung, sleep für Pausen und natürlich die Klassen TM1637 und HX711.
Ein TM1637-Objekt wird instanziiert und die Pin-Objekte für die Bedienung des HX711 werden erzeugt.
Die Pin-Objekte beim Konstruktoraufruf des Display-Objekts muss ich nicht angeben, weil ich einen ESP8266 verwende und daher die Default-Pins, GPIO5 und GPIO4, benutzt werden. Als Taste dient wieder die Flash-Taste oder eine externe Taste an GPIO0.
tm=TM1637()
dout=Pin(14)
dpclk=Pin(2)
taste=Pin(0,Pin.IN,Pin.PULL_UP) # D3
Eine einzige Funktion gibt es. putNumber() erledigt die Messwertausgabe. Der Wert vom HX711 wird in ein Segments-Tupel codiert und zum Display geschickt.
def putNumber(n):
s=tm.number2Segments(n)
tm.writeSegments(s)
Es folgt der Versuch, die Waage zu initialisieren. Dem Konstruktor übergeben wir die Pin-Objekte für die Daten- und Taktleitung. Der Chip wird aufgeweckt. Wir arbeiten mit Kanal 1, die Wägezelle liegt am Eingang A des HX711, und wir arbeiten mit voller Verstärkung. Bei jedem Start des Programms wird automatisch die Tara bestimmt und zwar mit 25 Einzelmessungen. Ein Lampentest informiert über die Funktion aller Filamente und darüber, dass bislang alles fehlerfrei gelaufen ist.
try:
hx = HX711(dout,dpclk)
hx.wakeUp()
hx.kanal(1)
hx.tara(25)
tm.lampTest()
sleep(1)
print("Waage gestartet")
Sollte ein Fehler aufgetreten sein, meldet der Except-Block "Error" am Display und das Programm wird beendet.
except:
print("HX711 initialisiert nicht")
s=(b"\x79\x50\x50\x5C\x50\x00",-1)
tm.writeSegments(s)
sys.exit()
In der Hauptschleife gibt es zwei Jobs. Wenn die Taste gedrückt ist, wird ein neuer Tara-Wert ermittelt und gespeichert. Das erlaubt uns, das Verpackungsgewicht abzuziehen oder das Zuwiegen von Zutaten. In der Anzeige erscheint "tArA". Nach dem Messvorgang arbeitet das Programm erst weiter, wenn die Taste losgelassen wurde.
while 1:
if taste.value() == 0:
s=(b"\x00\x78\x77\x50\x77\x00",-1)
tm.writeSegments(s)
hx.tara(25)
while taste.value()==0:
pass
Meine 20kg-Wägezelle liefert Werte, die auf der Zehntel-Gramm-Stelle wackeln. Anders ausgedrückt, 0,1 Gramm ist die unsichere Stelle. Ich habe sie deshalb ausgeblendet und gebe mich damit zufrieden, dass die Waage auf 1 Gramm genau misst. Das sind 0,005% vom maximalen Wert von 20000 Gramm. Diese Auflösung ist voll super!
Natürlich muss die Waage geeicht werden, bevor man sie wirklich verwenden kann. Im Vergleich zum Vorgängermodul hx711.py habe ich ein paar neue Features eingebaut, die dafür hilfreich sind und uns die Rechenarbeit, sowie Änderungen am Programm abnehmen.
Die Eichsequenz beginnt mit dem Starten von scale1637.py im Editorfenster von Thonny. Brechen Sie das Programm mit Strg+C ab, wenn der Lampentest begonnen hat, also alle Segmente leuchten.
Jetzt können Sie alle Methoden der Klasse HX711 händisch vom Terminal aus aufrufen.
Legen Sie jetzt nichts auf die Waage, und setzen Sie jetzt folgende Kommandos ab:
>>> hx.tara(25)
108978
>>> hx.tare
108978
Legen Sie jetzt ein Wägestück auf die Waage, dessen Masse sie möglichst genau wissen. Ich habe hier zwei Eichgewichte von je 500g genommen. Die Masse in Gramm übergeben Sie an calculateFactor().
>>> hx.calculateFactor(1000)
102.966
Das war's auch schon. Die Methode hat eine Wägung mit 25 Einzelmessungen gemacht, den Tara-Wert davon abgezogen und das Ergebnis durch die übergebene Masse dividiert. Das Endergebnis hat sie in der Datei config.txt im Flash des Controllers abgelegt.
def calculateFactor(self,masse):
self.cal = (self.mean(25)-self.tare)/masse
with open("config.txt","w") as f:
f.write(str(self.cal)+"\n")
return self.cal
Beim Instanziieren des HX711-Objekts, versucht der Konstruktor, diese Datei zu öffnen und den Inhalt auszulesen. Sollte das nicht gelingen, wird ein Wert genommen, der in der Variablen HX711.KalibrierFaktor abgelegt ist. Sie können dafür den Wert hernehmen, den Sie eben bestimmt haben. Die Snippets finden Sie alle in der Datei hx711neu.py
KalibrierFaktor=102.966
def __init__(self, dOut, pdSck, ch=KselA128):
self.data=dOut
self.data.init(mode=self.data.IN)
self.clk=pdSck
self.clk.init(mode=self.clk.OUT, value=0)
self.channel=ch
self.tare=0
try:
self.readFactor()
except:
self.cal=HX711.KalibrierFaktor
self.waitReady()
k,g=HX711.ChannelAndGain[ch]
print("HX711 bereit auf Kanal {} mit Gain {}".\
format(k,g))
print("Kalibrierfaktor is {}".\
format(self.cal))
def readFactor(self):
with open("config.txt","r") as f:
self.cal=float(f.readline())
return self.cal
Abbildung 11: Anzeige in der Entwicklung
Abbildung 12: Waage mit Schaltung bei der Eichung
Abbildung 13: Die "inneren Werte" der Waage
Abbildung 14: Frontscheibe mit blauer Anzeige
Viel Spaß und Erfolg mit der neuen DIY-Waage!
]]>Eine Waage ohne bewegliche Teile - geht nicht sagen Sie? Geht doch, sagt das Ergebnis meines aktuellen Projekts und zwar mit einer sagenhaften Auflösung und Genauigkeit. Aus, mit dem Auge nicht wahrnehmbaren Verbiegungen eines Aluminiumquaders, einem 24-Bit-ADC (Analog-Digital-Wandler), einem ESP8266 oder ESP32 (abkürzend im folgenden Text ESP) und einem OLED-Display, wird eine Digitalwaage, die in meinem Fall Massen bis 1kg erfassen kann, mit einer Auflösung von 0,01g! Wie das geht und welche Tricks dahinterstecken, das verrät Ihnen dieser Beitrag aus der Reihe
heute
Ich habe es ursprünglich auch nicht für möglich gehalten und war vom Resultat absolut überrascht. Der eingesetzte Aluquader ist eine sogenannte Wägezelle. Zwei Bohrungen in der Mitte dünnen das Metall aus, und an der Wandung sind Dehnungsmessstreifen aufgeklebt, in Abbildung1 oben und unten. Die Materialstärke beträgt dort ca. 1mm.
Abbildung 1: Wägezelle von der Seite
Die Wägezelle ist rechts mit der Bodenplatte verschraubt, links ist die Trägerplatte der Waage angebracht.
Abbildung 2: Waage für 1kg
Ein Blatt Papier der Größe 10cm x 15 cm verbiegt die Wägezelle nun immerhin so viel, oder eher wenig, dass die Waage das Gewicht von 0,9g anzeigt.
Wie funktioniert das? Dehnungsmesstreifen sind hauchdünne Leiterbahnen, die auf einen Kunststoffträger aufgebracht sind. Diese Pads werden auf ein Trägermaterial aufgeklebt.
Abbildung 3: Dehnungsmessstreifen schematisch
Durch Verbiegen des Trägers werden die Leiter geringfügig gedehnt und damit dünner und länger. Das bewirkt eine Änderung des elektrischen Widerstands, der bekanntlich von den beiden Parametern abhängt, ρ ist der spezifische Widerstand, eine Materialkonstante.
Abbildung 4: Widerstandsformel
Das aufgelegte Papier dürfte höchstens eine Verbiegung des 12,5mm hohen Quaders auslösen, die in der Größenordnung eines Atoms liegt. Die daraus resultierende Längenänderung des Messstreifens ebenfalls. Und das reicht aus, um die Spannung an der Messbrücke mit den vier Messstreifen, zwei oberhalb, zwei unterhalb des Quaders, so weit zu verändern, dass der HX711 daraus eine messbare und vor allem reproduzierbare Spannungsänderung ableiten kann.
Abbildung 5: HX711 - Beschaltung
Als Controller eignen sich sowohl beliebige ESP8266-, wie auch ESP32-Modelle mit mindestens vier freien GPIOs. Die Anzeige des Messwerts erfolgt über ein OLED-Display. Als Wägezelle kann natürlich auch ein anderes Modell dienen. Es gibt sie ab 100g aufwärts bis zu 100kg Wägevolumen und mehr.
1 |
D1 Mini NodeMcu mit ESP8266-12F WLAN Modul oder D1 Mini V3 NodeMCU mit ESP8266-12F oder NodeMCU Lua Amica Modul V2 ESP8266 ESP-12F WIFI oder NodeMCU Lua Lolin V3 Module ESP8266 ESP-12F WIFI oder ESP32 Dev Kit C unverlötet oder ESP32 Dev Kit C V4 unverlötet oder ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder NodeMCU-ESP-32S-Kit oder |
1 |
|
1 |
Wägezelle 1kg |
1 |
HX711 AD-Wandler für Wägezellen |
1 |
|
diverse |
Jumper Wire Kabel 3 x 40 STK. je 20 cm M2M/ F2M / F2F evtl. auch |
optional |
Thonny oder
ssd1306.py Hardwaretreiber für das OLED-Display
oled.py API für das OLED-Display
geometer_30.py großer Zeichensatz für die Ziffernanzeige
hx711.py API für den AX711
scale.py Das Betriebsprogramm
zeichensatz.rar Arbeitsumgebung zum Erzeugen eigener Zeichensätze
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 18.06.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Hier die Schaltbilder für das Projekt, es funktioniert wahlfrei für ESP32 und ESP8266. Die GPIOs für den Anschluss des HX711 sind so gewählt, dass sie den Start des ESP8266 nicht behindern und für beide Controllertypen dieselben Bezeichnungen haben. Lediglich die Anschlüsse für den I2C-Bus sind unterschiedlich, werden aber vom Programm automatisch zugeordnet.
Abbildung 6: HX711-Waage am ESP8266
Abbildung 7: HX711-Waage am ESP32
Wie die meisten Sensorbaugruppen, braucht auch das HX711-Modul eine Betriebssoftware. Leider ist der HX711 nicht I2C-fähig. Die Datenübermittlung erfolgt aber auch seriell über die Leitungen dout und dpclk. Es werden stets 24 Bit plus 1 bis 3 Bit für die Auswahl des Kanals A oder B und die Einstellung der Verstärkung übertragen. Das MSBit (Most Significant Bit = hochwertigstes Bit) sendet der HX711 als erstes Bit.
Für die Programmierung des Moduls hx711.py habe ich das Datenblatt des Herstellers benutzt.
from time import sleep_us, ticks_ms
class DeviceNotReady(Exception):
def __init__(self):
print("Fehler\nHX711 antwortet nicht")
Die Exception-Klasse behandelt den Fall, dass der HX711 nicht ansprechbar ist. Es folgt die Deklaration der Klasse HX711, die von DeviceNotReady erbt. Mit dem Werfen der Exception wird eine Instanz davon erzeugt, und der Konstruktor sorgt für die Ausgabe der Fehlermeldung.
class HX711(DeviceNotReady):
KselA128 = const(1)
KselB32 = const(2)
KselA64 = const(3)
Dbits =const(24)
Frame = const(1<<Dbits)
ReadyDelay = const(3000) # ms
WaitSleep =const(60) # us
ChannelAndGain={
1:("A",128),
2:("B",32),
3:("A",64),
}
KalibrierFaktor=2205.5
Wir beginnen mit einigen Konstanten. Der Kalibrierfaktor wird durch einige Wägungen mit verschiedenen bekannten Massestücken und einem Kalkulationstool, zum Beispiel Libre Office, bestimmt. Dazu komme ich später.
def __init__(self, dOut, pdSck, ch=KselA128):
self.data=dOut
self.data.init(mode=self.data.IN)
self.clk=pdSck
self.clk.init(mode=self.clk.OUT, value=0)
self.channel=ch
self.tare=0
self.cal=HX711.KalibrierFaktor
self.waitReady()
k,g=HX711.ChannelAndGain[ch]
print("HX711 bereit auf Kanal {} mit Gain {}".\
format(k,g))
Der Konstruktor nimmt die beiden Pin-Objekte dOut und pdSck, sowie optional den Kanal in ch. dOut wird als Eingang geschaltet, denn er soll ja die Daten vom Ausgang des HX711 empfangen. Über die Leitung dpSck gibt der ESP den Takt vor. Wir deklarieren die Attribute channel, tare und cal, dann warten wir auf das Ready-Signal des HX711. Kommt es nicht, dann wirft waitReady() eine DeviceNotReady-Exception. Hat es geklappt, dann holen wir die Kanal- und Gain-Einstellung und geben eine Meldung im Terminal aus. Das Dictionary ChannelAndGain wandelt die Kanalnummer in Klartext um.
def TimeOut(self,t):
start=ticks_ms()
def compare():
return int(ticks_ms()-start) >= t
return compare
Die Closure TimeOut() realisiert einen Softwaretimer, der den Programmablauf nicht blockiert, wie es sleep() & Co. tun. Die zurückgegebene Funktion compare() wird einem Bezeichner zugewiesen, über den abgefragt wird, ob die übergebene Zeit in Millisekunden schon abgelaufen ist (True) oder nicht (False).
def isDeviceReady(self):
return self.data.value() == 0
Die Methode isDeviceReady() gibt True zurück, wenn die Datenleitung auf GND-Potenzial liegt. Das ist laut Datenblatt der Zustand, wenn der HX711 bereit ist, Daten zu senden. Mit der ersten positiven Flanke auf der Taktleitung stellt der HX711 das MSBit auf der dOut-Leitung zur Verfügung.
Abbildung 8: Logic 2-Scan
Mit jedem weiteren Puls werden die Bits der Reihe nach hinausgeschoben. In der Zwischenzeit muss der ESP den Zustand der Leitung einlesen und verarbeiten. Durch den Takt gibt der ESP das Tempo vor. Die Pulsfolge liegt bei 125µs, das entspricht einer Taktung mit ca. 8kHz.
Die Pulsfolgen habe ich mit einem Logic-Analyzer und der Software Logic 2 aufgenommen. Immer wenn es Probleme bei der Datenübertragung gibt, setze ich gerne das DSO (Digitales Speicher Oszilloskop) ein, oder ein um Welten billigeres, kleines Tool, den Logic-Analyzer (LA) mit 8 Kanälen. Das Ding wird an den USB-Bus angeschlossen und zeigt mittels der kostenlosen Software, was auf den Busleitungen los ist. Dort, wo es nicht auf die Form von Impulsen ankommt, sondern lediglich auf deren zeitliche Abfolge, ist ein LA Gold wert. Und während das DSO nur Momentaufnahmen des Kurvenverlaufs liefert, kann man mit dem LA über längere Zeit abtasten und sich dann in die interessanten Stellen hineinzoomen. Eine Beschreibung zu dem Gerät finden Sie übrigens in dem Blogpost "Logic Analyzer -Teil 1: I2C-Signale sichtbar machen" von Bernd Albrecht. Dort ist auch beschrieben, wie man den I2C-Bus abtastet.
Nach dem Einlesen der 24 Daten-Bits werden noch ein bis drei weitere Pulse an pdSck ausgegeben, die Steuerbits. Sie haben folgende Bedeutung:
Abbildung 9: Bedeutung der Steuerpulse
def waitReady(self):
delayOver = self.TimeOut(ReadyDelay)
while not self.isDeviceReady():
if delayOver():
raise DeviceNotReady()
Der Name ist Programm, waitReady() wartet auf ein True von isDeviceReady(), stellt aber zuvor noch den Timer auf den Wert in ReadyDelay, das sind drei Sekunden. Geht dOut in dieser Zeit nicht auf LOW, wird eine DeviceNotReady-Exception geworfen. Dadurch wird das aufrufende Programm abgebrochen, falls es die Exception nicht abfängt.
def convertResult(self,val):
if val & MinVal:
val -= Frame
return val
Der HX711 sendet die Daten als Zweierkomplement-Werte. convertResult() erkennt eigentlich negative Werte am gesetzten MSBit, dem Bit 23. Ist es gesetzt, wird vom eingelesenen Wert die Stufenzahl 224 subtrahiert, um wirklich eine negative Zahl zu bekommen.
0xC17AC3 = 12679875 hat ein gesetztes MSBit
12679875 - 0x1000000 = -4097341
def clock(self):
self.clk.value(1)
self.clk.value(0)
clock() erzeugt einfach nur einen positiven Puls von 12,4µs auf der pdSck-Leitung.
def kanal(self, ch=None):
if ch is None:
ch,gain=HX711.ChannelAndGain[self.channel]
return ch,gain
else:
assert ch in [1,2,3],\
"Falsche Kanalnummer: {}\n \
Korrekt ist 1,2 3".format(ch)
self.channel=ch
if not self.isDeviceReady():
self.waitReady()
for n in range(Dbits + ch):
self.clock()
Übergibt man an kanal() kein Argument, dann liefert die Funktion die aktuellen Kanal- und Gain-Werte zurück. Das Dictionary HX711.ChannelAndGain übernimmt die Übersetzung in Klartext.
Ist eine Kanalnummer übergeben worden, prüfen wir auf den korrekten Bereich, schauen nach, ob der HX711 bereit ist und schieben dann die entsprechende Anzahl Pulse auf die pdSck-Leitung, 24 plus die Steuerbits.
def getRaw(self, conv=True):
if not self.isDeviceReady():
self.waitReady()
raw = 0
for b in range(Dbits-1):
self.clock()
raw=(raw | self.data.value())<< 1
self.clock()
raw=raw | self.data.value()
for b in range(self.channel):
self.clock()
if conv:
return self.convertResult(raw)
else:
return raw
Die Rohwerte, wie sie der HX711 liefert, werden von getRaw() empfangen. Wird True oder gar kein Argument übergeben, dann ist der Rückgabewert rosa, also eine Ganzzahl mit Vorzeichen. Mit False wird der Wert bloody zurückgegeben, wie er eben der Bitfolge entspricht, roh.
Wir warten auf die Sendebereitschaft des HX711, und setzen den Empfangspuffer auf 0.
In der for-Schleife senden wir 23 Pulse auf pdSck. Mit jedem Durchgang oderieren wir an die Stelle des LSBits (Least Significant Bit = niederwertigstes Bit) den Zustand auf der Datenleitung und schieben dann die Bits um eine Position nach links. Damit wandert das erste empfangene Bit an die Position 23, das MSBit. Nach einem weiteren Taktpuls schiebt der HX711 das LSBit auf die Datenleitung, das wir nur noch auf das LSBit von raw oderieren müssen. Außer Konkurrenz müssen dann noch die Steuerbits für die nächste Messung getaktet werden.
def mean(self, n):
s=0
for i in range(n):
s += self.getRaw()
return int(s/n)
Um das Rauschen der Werte zu glätten, verwenden wir nicht nur einen einzigen Messwert, sondern den Mittelwert aus mehreren Messungen. Das besorgt die Methode mean(), der wir die Anzahl der Einzelmessungen übergeben.
def tara(self, n):
self.tare = self.mean(n)
Die unbelastete Waage liefert natürlich auch schon einen ADC-Wert, die Tara. Wir rufen die Methode also stets beim Booten des Wägeprogramms auf, um die Anzeige auf 0 setzen zu können. Der Tara-Wert wird im attribut self.tare gespeichert, damit ihn die Methode masse() zur Verfügung hat, n ist wieder die Anzahl von Einzelmessungen.
def masse(self,n):
g=(self.mean(n)-self.tare) / self.cal
return g
Die Methode masse() zieht vom Messwert die Tara ab und berechnet dann den wahren Messwert in Gramm durch Division mit dem Kalibrierfaktor. Wie der bestimmt wird, zeige ich später.
def calFaktor(self, f=None):
if f is not None:
self.cal = f
else:
return self.cal
Während der Kalibrierung ist es von Vorteil, wenn man den Kalibrierfaktor händisch angleichen kann, ohne das Modul hx711.py jedes Mal neu auf den ESP hochladen zu müssen. Ohne Argumentübergabe wird der aktuelle Wert zurückgegeben.
def wakeUp(self):
self.clk.value(0)
self.kanal(self.channel)
def toSleep(self):
self.clk.value(0)
self.clk.value(1)
sleep_us(WaitSleep)
wakeUp() und toSleep() kitzeln den HX711 aus dem Schlafmodus, oder versetzen ihn in denselben. Die Signalfolge auf den Leitungen ist durch das Datenblatt des HX711 vorgegeben.
Große Ziffern erleichtern das Ablesen der Messung erheblich. Statt der üblichen acht Pixel verwenden wir 30 als Zeichenhöhe. Die Datei geometer_30.py enthält die entsprechenden Informationen dazu.
Und so stellen Sie sich einen eigenen Zeichensatz aus dem TTF-Vorrat von Windows her.
Laden Sie das Archiv zeichensatz.rar herunter und entpacken Sie den Inhalt in ein beliebiges Verzeichnis. Um Tipparbeit zu sparen, empfehle ich ein Verzeichnis mit einem kurzen Namen im Root-Pfad der Festplatte oder eines Sticks. Bei mir ist es F:\fonts.
Öffnen Sie aus dem Explorer heraus ein Powershell-Fenster in diesem Verzeichnis, indem Sie mit gedrückter Shift-Taste einen Rechtsklick auf das Verzeichnis machen. Dann Linksklick auf PowerShell-Fenster hier öffnen.
Abbildung 10: Powershell-Fenster öffnen
Geben Sie am Prompt folgende Zeile ein und drücken Sie Enter:
.\makecharset.bat britannic 30 """0123456789,-+KG""" "F:\fonts\quellen\"
Abbildung 11: Der Zeichensatz ist fertig
Im Verzeichnis befindet sich jetzt eine Datei britannic_30.py mit den Pixeldaten des neuen Zeichensatzauszugs. Umgesetzt wurden nur die Zeichen in """0123456789,-+KG""", das spart Speicherplatz. Weitere Zeichensätze können Sie aus dem fonts-Verzeichnis von Windows in das Verzeichnis quellen kopieren und wie oben angegeben umwandeln. Beachten Sie bitte, dass der Dateiname ohne die Ergänzung .TTF angegeben wird.
Das Programm scale.py greift auf vier externe Module zu, die vor Beginn in den Flash des ESP hochgeladen werden müssen, hx711.py, oled.py, ssd1306.py und geometer_30.py.
from time import sleep
from oled import OLED
import geometer_30 as cs
import sys
from hx711 import HX711
Über die Variable sys.platform kann ein ESP seinen Typ selbst feststellen. Danach werden die GPIO-Pins für den I2C-Bus deklariert.
if sys.platform == "esp8266":
i2c=SoftI2C(scl=Pin(5),sda=Pin(4),freq=400000)
elif sys.platform == "esp32":
i2c=SoftI2C(scl=Pin(22),sda=Pin(21),freq=400000)
else:
raise UnkownPortError()
Wir instanziieren ein Display-Objekt, löschen die Anzeige und deklarieren die Pin-Objekte für die Verbindung zum HX711, außerdem taste, die Instanz für die Tara-Taste.
if sys.platform == "esp8266":
i2c=SoftI2C(scl=Pin(5),sda=Pin(4),freq=400000)
elif sys.platform == "esp32":
i2c=SoftI2C(scl=Pin(22),sda=Pin(21),freq=400000)
else:
raise UnkownPortError()
Die Funktion putNumber() positioniert das Pixelmuster des Zeichens mit der Nummer n (bezogen auf den oben erzeugten Auszug, nicht ASCII). Die Position der linken oberen Ecke des Musters steht in xpos, ypos.
def putNumber(n,xpos,ypos,show=True):
breite=cs.number[n][0]
for row in range(1,cs.height):
for col in range(breite-1,-1,-1):
c=cs.number[n][row] & 1<<col
d.setPixel(xpos+breite+3-col,ypos+row,c,False)
if show:
d.show()
return xpos+breite+2
Ein Blick in die Datei geometer_30.py erklärt das Vorgehen. Im String chars sind die Zeichen aufgelistet. Der Index im String ist die Zeichennummer. Das "-"-Zeichen ist somit Nummer 11, die "0" ist Nummer 0. In der Liste number identifiziert diese Nummer gleichzeitig das Tupel mit der Breitenangabe und der Pixelmatrix. Diese wird nun zeilenweise Spalte für Spalte abgetastet und das Pixel gesetzt, wenn eine 1 gefunden wurde. Das geschieht verdeckt. Nur wenn show den Wert True hat, wird der gesamte Puffer zum Display geschickt. Der Rückgabewert ist die nächste freie Zeichenposition.
Abbildung 12: Matrix für das Zeichen 0
Die Zeichenposition wird auf 0 gestellt, als Startinformation gebe ich eine Reihe von "-"-Zeichen aus. Dann versucht der ESP den HX711 zu kontakten. Gelingt das, wecken wir ihn auf, stellen Kanal A mit Gain 128 ein und stellen den Tarawert fest, der im Attribut self.tare landet.
pos=0
try:
for n in range(8):
pos=putNumber(11,pos,0)
hx = HX711(dout,dpclk)
hx.wakeUp()
hx.kanal(1)
hx.tara(25)
print("Waage gestartet")
except:
print("HX711 initialisiert nicht")
for n in range(8):
pos=putNumber(0,pos,0)
sys.exit()
freq(160000000)
Klappt die Verbindung nicht, bekommen wir eine Meldung im Terminal und im Display eine Reihe von Nullen.
160MHz ist für den ESP8266 Topspeed, der ESP32 packt auch noch 240MHz.
Dann geht's in die Hauptschleife. Wir sehen nach, ob die Tara-Taste gedrückt ist, bestätigen das mit einer Reihe von Kommas im Display und holen einen neuen Tara-Wert. Dieses Feature ist nützlich, weil die Wägezelle einem Temperaturdrift unterliegt und damit der Nullpunkt jederzeit nachjustiert werden kann.
while 1:
if taste.value() == 0:
d.clearAll(False)
pos=0
for n in range(14):
pos=putNumber(10,pos,0)
hx.tara(25)
Der Format-String für die Ausgabe wird mit dem Messwert gefüttert. Verdeckt das Display löschen, Zeichenposition auf 0, Dezimalpunkt durch ein Komma ersetzen und die Zeichen bis zum vorletzten verdeckt in den Puffer schreiben.
m="{:0.2f}".format(hx.masse(10))
d.clearAll(False)
pos=0
m=m.replace(".",",")
for i in range(len(m)-1):
z=m[i]
n=cs.chars.index(z)
pos=putNumber(n,pos,0, False)
Mit der Ausgabe des letzten Zeichens wird der Pufferinhalt zum Display geschickt. Nach einer halben Sekunde Pause startet die nächste Runde.
z=m[len(m)-1]
n=cs.chars.index(z)
pos=putNumber(n,pos,0)
state=(taste.value() == 0)
sleep(0.5)
Das komplette Programm können Sie hier herunterladen.
Abbildung 13: Massenstücke fürs Kalibrieren der Waage
Sie brauchen dazu ein paar Massenstücke und gegebenenfalls eine Küchen- oder Briefwaage, falls die Massenstücke nicht geeicht sind. In diesem Fall können Sie sich einen Wägesatz aus fetten Muttern oder Schrauben oder ähnlichem herstellen. Natürlich müssen die Massen vor Verwendung mit einer anderen Waage bestimmt werden.
Abbildung 14: Kalibrieren am ESP8266
Starten Sie jetzt einmal das Programm scale.py in Thonny und brechen Sie in der Hauptschleife mit Strg+C ab. Das HX711-Objekt ist unter hx instanziiert und von der Terminal-Konsole aus ansprechbar. Wir starten mit folgenden Befehlen.
>>> hx.tara(50)
>>> hx.tare
562708
>>> hx.mean(25)-hx.tare
-17
>>> hx.mean(25)-hx.tare
8
Vom Mittelwert aus 25 Einzelmessungen subtrahieren wir den Tara-Wert. Die Ergebnisse sollten im Bereich zwischen -50…+50 liegen, wenn die Waage nicht belastet ist.
Jetzt legen wir die Massenstücke nacheinander und in Gruppen auf die Waage, so dass sie zunehmend mehr belastet wird. Jedes Mal starten wir erneut den letzten Befehl und notieren die Masse in Gramm und den im Terminal angezeigten Wert.
Die Tabelle geben wir in Libre Office Calc oder einem anderen Programm ein und lassen uns die Messkurve zusammen mit dem Bestimmtheitsmaß (Korrelationskoeffizient) und der Formel anzeigen.
Abbildung 14: Kalibrierkurve
Der Korrelationskoeffizient R2 ist faktisch 1, das spricht für die Präzision unserer Messung. Den Achsenabschnitt von -43,… können wir bei einer Größenordnung von 100000 vernachlässigen. Der Steigungsfaktor 1104 der Geraden ist unser gesuchter Kalibrierfaktor. Teilen Sie diesen Wert nun dem Objekt hx mit.
>>> hx.calFaktor(1104)
>>> hx.calFaktor()
1104
Wenn Sie jetzt die Methode masse() aufrufen, sollte ein Wert von 0,0… angezeigt werden.
>>> hx.masse(25)
0.0244565
Tragen Sie den Kalibrierfaktor in der Datei hx711.py ein, laden Sie diese zum ESP hoch und starten Sie das Programm scale.py neu.
KalibrierFaktor=1104
Wenn jetzt beim Auflegen der Massenstücke deren Masse angezeigt wird, haben Sie gewonnen und können sich belobigend auf die Schulter klopfen.
Liegt der angezeigte Wert leicht neben dem erwarteten Gewicht, dann können Sie noch ein wenig am Kalibrierfaktor-Wert feilen. Wenn Sie ihn vergrößern, wird der Massenwert sinken und umgekehrt.
Im zweiten Teil zeige ich Ihnen, wie ich das OLED gegen eine große LED-Anzeige getauscht habe. Bis dahin.
]]>Für diesen Blog ist das AZ-Touch MOD Wandgehäuseset ein Muss, siehe Tabelle 1.
Pos |
Anzahl |
Bauteil |
Link |
1 |
1 |
AZ-Touch MOD Wandgehäuseset |
|
2 |
1 |
Optional ESP32 NodeMCU Module WLAN WiFi Development Board |
Tabelle 1: Hardware für diesen Blog
Neben dem eigentlichen Wandmod braucht es noch weiteres Werkzeug und Material um diesen Blog nachbauen zu können, siehe:
Pos |
Bauteil |
1 |
Lötkolben mit Lötzinn |
2 |
Klingeldraht, am besten in verschiedenen Farben |
3 |
Buchsen- und Stiftleisten |
4 |
Stiftleisten gewinkelt |
5 |
Zange |
6 |
Bohrer |
7 |
Werkzeug und Material um eigene Kabel zu crimpen |
Tabelle 2: Weiteres Material
Mit dem Update auf die Version 3 des AZ-Touch MOD Wandgehäusesets, hat sich auf der Platine im Vergleich zu den vorherigen Versionen, viel getan.
Gerade die Punkte 3, 4 und 6 sind für diesen Blog interessant, da der zuvor beschriebene Weg von Gerald Lechner etwas erleichtert wird. Jedoch muss man weiterhin löten und zusätzlich auch Kabel für Stecker crimpen. Wer diese Arbeit nicht scheut oder jemanden kennt, der das nötige handwerkliche Geschick hat, wird alle Vorteile nutzen können.
In der alten Version des AZ-Touch MOD Wandgehäusesets musste man umständlich einen Pin der Buchsenleiste verbiegen und verlöten. Dieser Punkt ist durch das neue Layout der Platine entfallen. Am einfachsten ist es, wenn Sie sich Abbildung 1 genau ansehen.
Abbildung 1: Unterseite Platine mit angemarkerten Pins für SD-Karte
Setzen Sie an dieser Stelle die Buchsenleiste ein, so passt das Display mit angelöteten Stiftleisten perfekt zusammen, siehe Abbildung 2.
Abbildung 2: Display auf Platine, vor dem Löten
Am besten montieren Sie das Display an den vorgesehenen Haltepunkten und löten dann die Stift- und Buchsenleisten fest.
Damit haben Sie schon mal die Verbindung von der Platine zu dem Display. Jedoch fehlt noch die spätere Kommunikation zu dem ESP32 NodeMCU Module. An dieser Stelle nutzen Sie die nicht verlöteten Kontakte für das Display, siehe Abbildung 3.
Abbildung 3: Pins des nicht verwendeten TFT- Displays nutzen
Verlöten Sie dazu, wie auf dem Bild gezeigt, eine gewinkelte Stiftleiste beginnend vom sechsten Pin von oben. Der achte Pin ist bewusst abgepetzt worden, da dieser nicht gebraucht wird. Verlöten Sie zwei weitere gewinkelte Buchsenleisten auf der Seite des Mikrocontrollers bei der Beschriftung IO32 – IO33 und A0 – IO39. Zuletzt verlöten Sie die Pins der gezeigten Kabel, siehe Abbildung 4.
Abbildung 4: Die Pins der SD korrekt verlöten
Als Hilfestellung soll Tabelle 3 die genaue Verdrahtung noch einmal erläutern. Wichtig ist dazu die Orientierung aus Abbildung 4.
Pos Links von oben |
Funktion |
Pos Recht von oben |
Funktion |
Bemerkung |
1 |
SD_CS |
% |
% |
Wird nicht beim TFT verlötet |
2 |
SD_MOSI |
6 |
TFT_MOSI |
|
3 |
SD_MISO |
9 |
TFT_MISO |
|
4 |
SD_SCK |
7 |
TFT_SCK |
|
Tabelle 3: Anschluss der Pins
Wie Sie vllt. schon auf der Abbildung 4 erkannt haben, wird der erste Pin SD_CS in meinem Beispiel an IO 25 gesteckt. Dies muss im späteren Quellcode beachtet werden, da sonst der Zugriff auf die SD-Karte fehlschlägt.
Komme ich nun auf eine weitere Modifikation, die ich im Zuge meines Upgrades gemacht habe.
Sehen Sie sich Abbildung 5 genauer an, so wird ihnen auf der linken Seite der kleine Taster auffallen.
Abbildung 5: Reset-Button am AZ-Touch Mod
Diesen habe ich verbaut, um einen extern eingeleiteten Reset durchführen zu können. Ich hatte das Problem, dass es beim ersten Start immer Probleme mit der SD Karte gab. Konkret bedeutet das, die SD konnte nicht initiiert werden und der Mikrocontroller musste einmal neugestartet werden. Wahrscheinlich müsste ich an dieser Stelle im Quellcode nur ein paar Anpassungen machen, jedoch ist so ein externer Reset auch in anderen Momenten praktisch. Sucht man bei einem großen Onlineversandhandel nach „7 mm Mini Round Momentary 2 Pins“, so erhalten Sie als Suchergebnis den von mir verbauten Taster. Mit einem 8mm Bohrer habe ich an der Position aus Abbildung 6 ein Loch ins Gehäuse gebohrt.
Abbildung 6: Position der Bohrung für den Taster
Achten Sie darauf, dass die Bohrung ggf. auf Höhe einer Versteifungsrippe in der Innenseite des Gehäuses sein könnte. Diese Rippe müssen Sie mit einer kleinen Zange entfernen, da sonst der Taster später nicht sauber verschraubt werden kann. Die Verdrahtung ist nun etwas komplizierter. Um den Mikrocontroller zu reseten, muss der Pin EN auf Masse gezogen werden. Für die Masse kann ein Draht vom Taster auf den Pin GND der Platine gesteckt werden, aber der zweite Draht muss direkt am Lötpin für EN verlötet werden, siehe Abbildung 7.
Abbildung 7: Draht direkt an Pin EN verlötet
Da direkt an der Buchsenleiste gelötet werden muss, siehe rotes Kabel, empfiehlt es sich den Kontakt und die offene Litze mit ausreichend Lötzinn vorzubereiten. Ist alles fest verlötet und der Taster verbaut, so kann das restliche Kabel unterhalb des Displays verstaut werden. Damit muss der Taster nicht jedes Mal beim Entfernen der Platine vom Gehäuse demontiert werden.
Der hier gezeigte Umbau ist vllt. etwas komplexer als jener, den Gerald Lechner beschrieben hat. Durch die aktuelle Version 3 der Platine hat man aber durchaus nun eine etwas leichtere Lösung, die SD-Karte zu verwenden und auf weitere IO’s zuzugreifen.
Anfangs hat es mich etwas Zeit gekostet, zu prüfen was möglich ist, jedoch war die Grundarbeit von Gerald Lechner eine große Hilfe. Wer also das AZ-Touch MOD Wandgehäuseset voll ausnutzen möchte, sollte über dieses Upgrade durchaus nachdenken.
]]>und DS18B20. Bei der Gelegenheit behandeln wir dann den DHT22 gleich mit. Und schließlich betrachten wir noch das Bauteil, das aussieht und ggf. auch angewendet wird wie ein Potentiometer, aber keines ist: der Rotary Encoder bzw. wenig zutreffend auf Deutsch der Drehschalter, Drehencoder oder Drehwinkelcodierer.
1 |
Raspberry Pi Pico oder Pico W |
1 |
|
alt |
|
1 |
|
Jumperkabel, Taster/Button |
|
optional |
Beginnen wir mit dem Temperatursensor DS18B20, den es in verschiedenen Bauformen gibt. Die Urform ähnelt einem kleinen Transistor in einem schwarzen Gehäuse mit drei Beinchen. Im Sensor-Kit ist das Bauteil zusammen mit einer LED und dem Vorwiderstand auf einer kleinen Platine aufgelötet. Mein Favorit sind die wasserdichten, in einem Edelstahlgehäuse eingesetzten Sensoren mit einem ca. 1 m langen Kabel.
Alle werden gleich angeschlossen und in das Programm eingebunden. Man benötigt allerdings zwei MicroPython-Module, die unter dem Menüpunkt Werkzeuge/Verwalte Pakete … heruntergeladen werden: onewire und ds18b20.
Der Name Eindrahtbus (onewire) verwirrt etwas, weil das Bauteil schließlich drei Anschlüsse hat, aber zwei dienen der Spannungsversorgung mit 3,3V und GND. Das (meist) gelbe Kabel dient der Datenübertragung und wird an einen beliebigen GPIO-Pin angeschlossen. Das Signal ist weder digital (High oder Low) noch analog (Spannung zwischen 0 und 3,3V), sondern es handelt sich um ein Übertragungsprotokoll mit vielen Bits und Bytes, die man z.B. mit dem Logic Analyzer und dem dazugehörigen Programm anschauen kann. Die Auswertung erfolgt mit Hilfe der Module, so dass das Programmbeispiel sehr kurz ausfällt.
from machine import Pin
from onewire import OneWire
from ds18x20 import DS18X20
import utime as time
one_wire_bus = Pin(13) # beliebiger Pin, PULL_UP im Modul OneWire
sensor_ds = DS18X20(OneWire(one_wire_bus))
devices = sensor_ds.scan()
while True:
sensor_ds.convert_temp()
time.sleep(1)
# Sensoren abfragen
for device in devices:
print(' Sensor:', device)
print('Temperatur:', sensor_ds.read_temp(device), '°C')
print()
time.sleep(5)
Zwei Anmerkungen: Erstens wird häufig beschrieben, dass der Signal-Pin über einen Pull-up-Widerstand von 4,7 kOhm mit Vcc verbunden werden soll. Das kann entfallen, weil hier der interne Pull-up-Widerstand für den gewählten Pin im Modul aktiviert wird. Zweitens kann man mehrere DS18B20 parallelschalten und den Temperaturwert über eine eindeutige Kennung dem jeweiligen Sensor zuordnen. Siehe hierzu das Programmbeispiel, bei dem insgesamt drei Sensoren angeschlossen sind.
Ebenfalls nur drei Pins hat der kombinierte Temperatur- und Luftfeuchtigkeitssensor DHT11. Auch hier soll der Signal-Pin über einen Pull-up-Widerstand von 10 kOhm mit Vcc verbunden werden. Dieser ist allerdings auf der kleinen Platine bereits eingelötet und somit hier verzichtbar. Zusätzlich kann man den internen Pull-up-Widerstand bei der Instanziierung des Sensors aktivieren:
from dht import DHT11
dht11_sensor = DHT11(Pin(13, Pin.IN, Pin.PULL_UP))
Oberes Bild: DHT11 auf Mini-Platine; die Zahl 34 bezieht sich auf den Abschnitt im eBook Sensor-Kit mit Arduino.
Unteres Bild: Installation des Pakets (Modul) dht für DHT11 und DHT22
from machine import Pin
import utime as time
from dht import DHT11
dht11_sensor = DHT11(Pin(13, Pin.IN, Pin.PULPP_UP)) #data pin = GP13
time.sleep(1)
while True:
dht11_sensor.measure()
t = dht11_sensor.temperature()
rH = dht11_sensor.humidity()
print(" Temperature: ", t, "°C")
print("rel. Luftfeuchtigkeit: ", rH, "%")
print()
time.sleep(5) #Sleep for five seconds
Nicht im Sensor-Kit, aber mit dem gleichen Python-Modul im Programm einzubinden, ist der Sensor DHT22. Äußerlich dem DHT11 sehr ähnlich, jedoch mit weißem Gehäuse und vier Pins, von denen allerdings einer nicht belegt ist.
Wie man in der Programm-Bibliothek (Modul dht.py) erkennen kann, ist der DHT22 genauer in der Auflösung. Temperatur und rel. Luftfeuchtigkeit werden jeweils in zwei Registern abgespeichert, anstatt in einem Register wie beim DHT11. Das führt zu Messergebnissen mit einer Kommastelle.
Bild: Ausschnitt aus dem Modul dht.py, Berechnung der rel. Luftfeuchtigkeit und der Temperatur
Hier eine Beispielprogramm zum DHT22:
from machine import Pin
import utime as time
from dht import DHT22
dht22_sensor = DHT22(Pin(13, Pin.IN, Pin.PULPP_UP)) #data pin = GP13
time.sleep(1)
while True:
dht11_sensor.measure()
t = dht22_sensor.temperature()
rH = dh221_sensor.humidity()
print(" Temperature: ", t, "°C")
print("rel. Luftfeuchtigkeit: ", rH, "%")
print()
time.sleep(5) #Sleep for five seconds
Ebenfalls den beiden genannten DHT 11 und 22 äußerlich ähnlich, jedoch mit einem schwarzen Gehäuse, ist der DHT 20. Dieser funktioniert mit einer I2C-Schnittstelle und benötigt eine andere Bibliothek, die man nicht in der Paketverwaltung unter Thonny findet, sondern auf Github: https://github.com/flrrth/pico-dht20.
Den DHT 20 haben wir bei Aufnahme in das Sortiment ausführlich getestet und in einem Blog beschrieben (mit Arduino-IDE und Logic Analyzer):
DHT20 - ein neuer Temperatur- und Luftfeuchtigkeitssensor
In einem weiteren Blog wird der DHT 20 am Raspberry Pi Pico W mit Micro Python verwendet:
Raspberry Pi Pico W mit BME280 und OLED in Thonny und MicroPython
Das letzte, noch nicht betrachtete Bauteil aus dem Sensor-Kit ist der Rotary Encoder
Drehgeber sind mechanische Geräte, die ein wenig wie Potentiometer aussehen und häufig an deren Stelle verwendet werden. Sie liefern jedoch keine analogen Werte, die einer bestimmten Position zugeordnet werden können und besitzen keine Endanschläge. Meist erkennt man sie daran, dass sie nach einer kleinen Drehung scheinbar einrasten, man kann jedoch mühelos weiterdrehen. Ich habe bei meinem Rotary Encoder 20 „Klicks“ bei einer Umdrehung gezählt, also 360°/20=18° von Einrastposition zu Einrastposition.
Innerhalb dieser 18°-Drehung passiert das, was zum gewünschten Effekt führt. In jeder der 20 Einrastpositionen liegt über die Pull-up-Widerstände von 10 kOhm die Spannung Vcc=3,3V an den Kontakten DT und CLK an. Dazwischen werden diese Kontakte unterschiedlich mit Masse verbunden, erst einer, dann beide, dann nur der andere. Aus dieser Abfolge wird nicht nur die Drehung an sich erkannt, sondern auch die Drehrichtung. Wie gut, dass es für die Erkennung und Auswertung Mikrocontroller und passende Programm-Bibliotheken für Arduino-IDE und MicroPython gibt.
Der Vollständigkeit halber: Wenn man den Rotary Encoder nicht dreht, sondern hineindrückt, wird der Kontakt SW gegen Ground geschaltet, also ein simpler Taster (Button), wie im zweiten Teil beschrieben.
Hier nun der Link zur Programm-Bibliothek auf Github und ein Beispielprogramm:
https://github.com/miketeachman/micropython-rotary
# MIT License (MIT)
# Copyright (c) 2021 Mike Teachman
# https://opensource.org/licenses/MIT
# example for MicroPython rotary encoder
from rotary_irq_rp2 import RotaryIRQ
import time
r = RotaryIRQ(pin_num_clk=13,
pin_num_dt=14,
min_val=0,
max_val=19,
reverse=True,
range_mode=RotaryIRQ.RANGE_WRAP)
val_old = r.value()
while True:
val_new = r.value()
if val_old != val_new:
val_old = val_new
print('result =', val_new)
time.sleep_ms(50)
Damit haben wir alle Teile des 35 in 1 Sensor-Kits mit dem Raspberry Pi Pico unter MicroPython angewendet. Arduino-IDE-Anwender finden Erklärungen und Programmbeispiele in dem (m.E.) sehr guten eBook, das auf der Produktseite heruntergeladen werden kann.
In Verbindung mit den vorherigen Blog-Beiträgen zum Raspberry Pi Pico können Sie Ihre Messergebnisse auf einem kleinen LCD- oder OLED-Display anzeigen oder im heimischen WLAN bereitstellen. Viel Spaß bei Ihren Projekten, ich „stürze“ mich als nächstes auf den SD-Card-Reader, um meine Messergebnisse aufzuzeichnen.
Hier noch einmal unsere bisherigen Beiträge zum Thema Pico/Pico W:
Wir entwerfen drei Ornamente: einen Würfel mit weihnachtlichen Motiven auf seinen Seiten, eine Laterne und unsere eigene Weihnachtskugel. Für die Beleuchtung nutzen wir einen Ring mit 8 RGB-LEDs und zur Steuerung einen Mikrocontroller AZ-Nano V3-Board Atmega328. Das Projekt soll als Anstoß dienen, damit Sie Ihrer eigenen Kreativität freien Lauf lassen können und eigene Objekte entwerfen.
Lassen Sie uns mit diesem Weihnachtsprojekt beginnen.
Die drei WS2812 LED-Ringe sind mit den digitalen Pins 7, 8 und 9 des AZ-Nano Mikrocontrollers verbunden. Die erforderliche 5 VDC-Spannung wird vom Netzteiladapter geliefert. Statt den Nano über den USB-Port mit Strom zu versorgen, können Sie auch den VIN-Pin verwenden.
Die Weihnachtskugel besteht aus Küchenpapier. Für den Bau verwenden wir einen Ballon, den wir auf die gewünschte Größe aufblasen. Wir verdünnen Holzleim in Wasser im Verhältnis 1:1 und kleben Küchenpapierstücke mit dem verdünnten Leim auf den Ballon. Schichten Sie das Papier auf, bis die gewünschte Dicke der Kugel erreicht ist. Lassen Sie sie ein paar Tage trocknen, bis sie vollständig getrocknet ist. Durchstechen Sie dann den inneren Ballon, so dass er platzt und die Kugel ist fertig. Je nachdem, wie Sie das Küchenpapier aufbringen, entstehen Muster. Je mehr Schichten Sie aufbringen, desto dunkler werden diese Stellen.
Für die Installation des LED-Rings innerhalb der Kugel gibt es zwei Möglichkeiten: Die erste besteht darin, die Kugel mit einem Messer in der Mitte zu zerschneiden, um den LED-Ring in einer Hälfte zu installieren, die Kabel und die Aufhängeschnur durchzuführen und dann die Hälften mit etwas Leim wieder zu verbinden. Die andere Möglichkeit besteht darin, nur einen Teil der Kugel mit einem Durchmesser etwas größer als der des LED-Rings abzuschneiden, den LED-Ring zu installieren und in der Mitte der Kugel ein Loch für die Kabel und die Aufhängeschnur einzuschneiden.
Die Teile des Würfels und der Laterne sind aus Balsaholz bzw. Pappelsperrholz gefertigt. Für die bessere Montage sind die Kanten nicht gerade geschnitten, sondern mit Nut und Feder versehen. Für die Laterne werden die Kanten leicht angeschliffen, da Ober- und Unterteil in einem schrägen Winkel zueinanderstehen.
Im Inneren der Seitenteile wird weißes Papier mit den entsprechenden Abmessungen aufgeklebt. In diesem Projekt habe ich aus dem Papier Formen gefaltet der Laterne und des Würfels gefaltet, die dann dort hineingelegt und befestigt werden. Dadurch wird das Licht diffus und besser sichtbar.
Zuletzt können Sie das Holz noch mit Farbe versehen.
Im folgenden Bild sind die Maße zu sehen (hier sind die Nuten und Federn nicht eingezeichnet).
Hinweis: wenn Sie einen anderen LED-Ring verwenden, achten Sie auf die Größe und passen Sie eventuell Ihre Bauteile daran an.
Jedes Ornament, das wir gebaut haben, ändert die Farbe seiner Beleuchtung in den von uns ausgewählten Farben und in regelmäßigen Zeitabständen. Schauen wir nun auf den Sketch.
Zuerst werden die erforderlichen Bibliotheken inkludiert. Die einzige Bibliothek, die in diesem Projekt installiert werden muss, ist die NeoPixel-Bibliothek von Adafruit, um die LED-Ringe nutzen zu können. Die LEDs sind nummeriert und können einzeln angesteuert werden.
#include <Adafruit_NeoPixel.h>
Die folgenden vier Zeilen enthalten die Variablendefinitionen für die Verbindungen der Ringe zum Mikrocontroller und die Variable zur Festlegung der Anzahl der LEDs der Ringe.
#define cube_ring_LED 7 #define lantern_ring_LED 8 #define ball_ring_LED 9 #define number_LEDs_ring 8
Hinweis: wenn Sie einen LED-Ring mit mehr LEDs verwenden, müssen Sie für die Konstante number_LEDs_ring hier statt 8 den Wert für die Anzahl eintragen.
Um jeden LED-Ring einzeln anzusprechen, muss je ein Objekt der NeoPixel-Bibliothek erzeugt werden. In den Argumenten müssen die Anzahl der LEDs pro Ring, die Pins am Mikrocontroller, an denen jeder Ring angeschlossen ist, sowie die Eigenschaften jedes Rings, nämlich RGB LEDs (NEO_GRB) und die Betriebsfrequenz von 800 kHz (NEO_KHZ800), angegeben werden.
Adafruit_NeoPixel cube_ring(number_LEDs_ring, cube_ring_LED, NEO_GRB + NEO_KHZ800); Adafruit_NeoPixel lantern_ring(number_LEDs_ring, lantern_ring_LED, NEO_GRB + NEO_KHZ800); Adafruit_NeoPixel ball_ring(number_LEDs_ring, ball_ring_LED, NEO_GRB + NEO_KHZ800);Als Nächstes wird die Definition der Variablen für die Farben jedes Rings mit den RGB-Werten jeder Farbe durchgeführt. Hier sehen Sie die Variablen für den Würfelring. Im Sketch finden Sie die Werte für die Ringe der Laterne und der Weihnachtskugel.
uint32_t cube_color_red = cube_ring.Color(150,0,0); uint32_t cube_color_green = cube_ring.Color(0,150,0); uint32_t cube_color_blue = cube_ring.Color(0,0,150); uint32_t cube_color_yellow = cube_ring.Color(150,150,0); uint32_t cube_color_purple = cube_ring.Color(150,0,150); uint32_t cube_color_light_blue = cube_ring.Color(0,150,150); uint32_t cube_color_white = cube_ring.Color(150,150,150); uint32_t cube_color_black = cube_ring.Color(0,0,0);
Nachdem die Bibliotheken implementiert und die Variablen deklariert wurden, müssen die LED-Ringe initialisiert werden. Der Anfangszustand, wenn Spannung angelegt wird oder der Mikrocontroller zurückgesetzt wird, wird in der setup()-Methode konfiguriert. Der Ausgangszustand für die LEDs der Ringe ist AUS.
cube_ring.begin(); lantern_ring.begin(); ball_ring.begin(); cube_ring.clear(); lantern_ring.clear(); ball_ring.clear();
Danach beginnt die Ausführung der loop()-Methode, die kontinuierlich läuft und damit die Farben dauerhaft verändert werden. Die aufgerufenen Methoden bestimmen die Farben des jeweiligen RGB-Rings. Die erste aufgerufene Methode ist die Beleuchtung des Würfels mit der Farbe Rot.
cube_red();
In der Methode wird zuerst die Helligkeit der LEDs auf die Hälfte des maximalen Werts (125 von 254) eingestellt. Dann wird eine Schleife ausgeführt, die von der ersten LED an Position 0 bis zur letzten LED an Position 7 (insgesamt 8 LEDs) läuft, sie auf die rote Farbe setzt und einschaltet. Es wird eine Pause von 200 Millisekunden zwischen den LEDs eingelegt, um einen sanften Übergang zwischen den Farbwechseln jedes Rings zu erreichen. Anschließend wird noch einmal pausiert, bevor die Methode verlassen und zur loop() zurückgekehrt wird. Anschließend wird die Methode lantern_yellow() aufgerufen, die genauso aufgebaut ist. Unterschied ist der Ring und die Farbe.
void cube_red() { cube_ring.setBrightness(125); for (int i=0; i<number_LEDs_ring; i++) { cube_ring.setPixelColor(i, cube_color_red); cube_ring.show(); delay(200); } delay(3000); }
Im Sketch sind die Befehle kommentiert, um sie nachvollziehen zu können. Wir hoffen, dass Sie Spaß daran haben, dieses Projekt zur Weihnachtsdekoration durchzuführen.
Tipp: Wenn Sie drei ESP8266 oder ESP32 mit Akkus verwenden, können Sie die Elektronik separat in den Lampen unterbringen und sogar per Webportal oder Android App steuern. Lassen Sie sich dazu von unseren anderen Projekten inspirieren
Wir wünschen Ihnen ein frohes Weihnachtsfest.
Sollte an dieser Stelle kein Video angezeigt werdern, müssen Sie die Cookies in ihrem Webbrowser zulassen.
]]>
Das Schalten von Relais-Modulen haben wir in der Vergangenheit schon mehrfach auf verschiedene Art und Weise gezeigt, z.B.
Relais sind sehr wichtige Bauelemente, wenn unterschiedliche Spannungen oder hohe Ströme zu schalten sind oder aus anderen Gründen eine galvanische Trennung von Laststromkreis und Steuerkreis notwendig ist. Das Beispiel Modelleisenbahn hatte ich in einem anderen Blog schon erwähnt. Andere Anwendungen in der Advents- und Weihnachtszeit sind Lichterketten oder z.B. ein Schrittmotor für die Weihnachtspyramide, wenn auf Kerzen für den Antrieb verzichtet werden soll. Aber auch Besitzer von Aquarien oder Terrarien können damit Beleuchtung, Heizung, Pumpe usw. ein- oder ausschalten.
Hier nun die kostengünstigste Lösung: Außer dem Relais-Modul mit Spannungsversorgung werden nur ein Raspberry Pi Pico W und einige Jumperkabel benötigt.
1 |
Raspberry Pi Pico W oder WH |
Beim Modell WH sind die Pinleisten bereits angelötet |
1 |
||
div. |
||
1 |
Android Smartphone |
MIT APP INVENTOR funktioniert grundsätzlich auch mit iPhone, jedoch nicht ausprobiert |
Zunächst noch einmal ein Blick auf das Relais-Modul. Wie die verschiedenen Motortypen sind auch Relais mit ihren elektromagnetischen Eigenschaften nicht wirklich unbedenkliche Verbraucher wie z.B. LEDs. Gerade beim Ein- und Ausschalten fließen deutlich höhere Ströme als unsere Mikrocontroller vertragen. Das bedeutet bei mehr als einem Relais unbedingt eine externe Spannungsversorgung, galvanische Trennung und für den Ausschaltvorgang eine Freilaufdiode (engl. flyback diode) verwenden. Dafür ist bei den Modulen Vorsorge getroffen.
Auf dem folgenden Bild erkennt man (von oben nach unten)
Bei der Spannungsversorgung des Relais-Moduls muss man unterscheiden zwischen den Steuerstromkreisen und den Elektromagneten. Die insgesamt 10 Pins in der Mitte sind GND, Eingänge IN1 bis IN8 und VCC (Spannungseingang der Steuerelektronik), die drei Pins rechts im Bild sind GND und Spannungseingang für die Magnete. Wenn, wie hier im Bild, ein Kurzschlussstecker (Jumper) zwischen VCC und JD-VCC anliegt, sind beide Versorgungsspannungen identisch und - man kann es nicht oft genug sagen - kommen aus einer externen Spannungsquelle, nicht aus dem Mikrocontroller(!). Und zu den Eingängen IN1 bis IN8 ist zu sagen, dass bei den meisten Modulen das Relais anzieht, wenn das Signal vom Mikrocontroller auf LOW gesetzt wird. Aber es gibt auch Ausnahmen, einfach ausprobieren, wann es Klick macht.
Bei meiner Breadboard-Schaltung habe ich den Jumper gesetzt gelassen und ein Batterie-Pack mit drei AA-Batterien (3*1,5 V = 4,5 V) an die Stromschienen angeschlossen. Wer Akkus verwenden möchte, sollte vier Akkus (4* 1,2 V = 4,8 V) verwenden. Am Ende habe ich auch den Raspberry Pi Pico W an dieser Stromschiene angeschlossen. Der Eingang VSYS verträgt Spannungen zwischen 1,8 V und 5,5 V. Zitat aus dem Datasheet: “VSYS is the main system input voltage, which can vary in the allowed range 1.8V to 5.5V, and is used by the on-board SMPS to generate the 3.3V for the RP2040 and its GPIO.“
Bis alles funktioniert, liefert jedoch der USB-Ausgang des Computers die Spannung für den Pico W, das Battery Pack die Spannung für das Relais-Modul. Erst am Ende wird das MicroPython-Programm unter dem Namen main.py auf dem Pico gespeichert, um die Autostart-Funktion zu aktivieren und der Pico wird dann auch an die externe Spannung angeschlossen.
Wer Thonny und MicroPython noch nicht installiert hat, kann sich die einzelnen Schritte in den Blog-Beiträgen Raspberry Pi Pico und Thonny mit MicroPython - Teil 1 und Raspberry Pi Pico W jetzt mit Bluetooth - Teil 1 anschauen. Bei MicroPython bitte daran denken, die aktuelle Datei mit der Endung .uf2 zu verwenden, da Bluetooth erst im Juni diesen Jahres implementiert wurde.
# 8 relays controlled by Raspberry Pi Pico W BLE and Smartphone
# Modified from Official Rasp Pi example here:
# https://github.com/micropython/micropython/tree/master/examples/bluetooth
# by Bernd54Albrecht for AZ-Delivery 2023
import bluetooth
import random
import struct
import time
from ble_advertising import advertising_payload
from micropython import const
from machine import Pin, PWM
import rp2
# initialize relay pins
r1 = Pin(6, Pin.OUT,value=1)
r2 = Pin(7, Pin.OUT,value=1)
r3 = Pin(8, Pin.OUT,value=1)
r4 = Pin(9, Pin.OUT,value=1)
r5 = Pin(10, Pin.OUT,value=1)
r6 = Pin(11, Pin.OUT,value=1)
r7 = Pin(12, Pin.OUT,value=1)
r8 = Pin(13, Pin.OUT,value=1)
## taken from ble_simple_peripheral.py
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_FLAG_READ = const(0x0002)
_FLAG_WRITE_NO_RESPONSE = const(0x0004)
_FLAG_WRITE = const(0x0008)
_FLAG_NOTIFY = const(0x0010)
_UART_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_TX = (
bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_READ | _FLAG_NOTIFY,
)
_UART_RX = (
bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE,
)
_UART_SERVICE = (
_UART_UUID,
(_UART_TX, _UART_RX),
)
class BLESimplePeripheral:
def __init__(self, ble, name="PicoW"):
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq)
((self._handle_tx, self._handle_rx),) = self._ble.gatts_register_services((_UART_SERVICE,))
self._connections = set()
self._write_callback = None
self._payload = advertising_payload(name=name, services=[_UART_UUID])
self._advertise()
def _irq(self, event, data):
# Track connections so we can send notifications.
if event == _IRQ_CENTRAL_CONNECT:
conn_handle, _, _ = data
print("New connection", conn_handle)
self._connections.add(conn_handle)
elif event == _IRQ_CENTRAL_DISCONNECT:
conn_handle, _, _ = data
print("Disconnected", conn_handle)
self._connections.remove(conn_handle)
# Start advertising again to allow a new connection.
self._advertise()
elif event == _IRQ_GATTS_WRITE:
conn_handle, value_handle = data
value = self._ble.gatts_read(value_handle)
if value_handle == self._handle_rx and self._write_callback:
self._write_callback(value)
def send(self, data):
for conn_handle in self._connections:
self._ble.gatts_notify(conn_handle, self._handle_tx, data)
def is_connected(self):
return len(self._connections) > 0
def _advertise(self, interval_us=500000):
print("Starting advertising")
self._ble.gap_advertise(interval_us, adv_data=self._payload)
def on_write(self, callback):
self._write_callback = callback
# This is the MAIN LOOP
def demo(): # This part modified to control 8 relays
ble = bluetooth.BLE()
p = BLESimplePeripheral(ble)
def on_rx(code): # code is what has been received
print("code = ",code) # Print code
code = str(code)[2:-5] # necessary only for Smartphone App
code = int(code)
print("code = ",code) # Print code
if code & 1 == 1:
print("Relais1 ON")
r1.off() # Relay1 ON
else:
print("Relais1 OFF")
r1.on() # Relay1 OFF
if code & 2 == 2:
print("Relais2 ON")
r2.off() # Relay2 ON
else:
print("Relais2 OFF")
r2.on() # Relay2 OFF
if code & 4 == 4:
print("Relais3 ON")
r3.off() # Relay3 ON
else:
print("Relais3 OFF")
r3.on() # Relay3 OFF
if code & 8 == 8:
print("Relais4 ON")
r4.off() # Relay4 ON
else:
print("Relais4 OFF")
r4.on() # Relay4 OFF
if code & 16 == 16:
print("Relais5 ON")
r5.off() # Relay5 ON
else:
print("Relais5 OFF")
r5.on() # Relay5 OFF
if code & 32 == 32:
print("Relais6 ON")
r6.off() # Relay6 ON
else:
print("Relais6 OFF")
r6.on() # Relay6 OFF
if code & 64 == 64:
print("Relais7 ON")
r7.off() # Relay7 ON
else:
print("Relais7 OFF")
r7.on() # Relay7 OFF
if code & 128 == 128:
print("Relais8 ON")
r8.off() # Relay8 ON
else:
print("Relais8 OFF")
r8.on() # Relay8 OFF
p.on_write(on_rx)
if __name__ == "__main__":
demo()
Anmerkungen zum Programmcode:
Zu Beginn werden wie üblich Programm-Module (vgl. Bibliotheken/libraries bei Arduino-IDE) importiert. Von den MicroPython-Beispielen für Bluetooth, siehe:
https://github.com/micropython/micropython/tree/master/examples/bluetooth
wird das Programm ble_advertising.py unverändert als Modul auf dem Pico W abgespeichert. Aus dem Programm ble_simple_peripheral.py habe ich die wichtigen Teile kopiert und in mein Programm integriert.
Für die Initialisierung der Relais-Pins verwende ich GP 6 bis 13 und setzte den Startwert auf 1 (HIGH), denn wie gesagt schalten meine Relais, wenn der Eingang auf 0 (LOW) gesetzt werden.
Danach folgen die Bluetooth-relevanten Codeanteile, die ich aus dem Beispiel kopiert habe.
Im Hauptteil des Programms erfolgen das Auslesen der vom Smartphone gesendeten Nachricht und das Auswerten und Verwenden des Codes. Dabei habe ich den acht Relais jeweils die Wertigkeit der Zweierpotenz mit der Nummer des Relais minus 1 zugeordnet. Code 0 bedeutet alle Relais ausgeschaltet, 255 bedeutet alle eingeschaltet. Beispiel: Relais 1 (Wertigkeit 2 hoch 0), 2 (Wertigkeit 2 hoch 1) und 8 (Wertigkeit 2 hoch 7) eingeschaltet bedeutet Code 2 hoch 0 plus 2 hoch 1 plus 2 hoch 7 = 1 + 2 + 128 = 131.
In acht if/else-Abfragen wird geprüft, ob die jeweilige Zweierpotenz im Code enthalten ist und das jeweilige Relais eingeschaltet. Der Rest der eigenen Leistung liegt in der Smartphone App, die online im Browser mit dem MIT APP INVENTOR erstellt bzw. angepasst wird.
Zum Einloggen wird z.B. ein gmail - Konto verwendet. Außerdem muss Bluetooth LE als Erweiterung (Menüpunkt Import extension) eingefügt werden. Details siehe den Blog-Beitrag
Raspberry Pi Pico W jetzt mit Bluetooth - Teil 4 - Robot Car mit Blaulicht und Sirene (PIO-gesteuert).
Wenn Sie den MIT APP INVENTOR gestartet haben, einfach meine Datei BLE_controller_8Relais.aia importieren und mit der App MIT AI2 Companion ausprobieren, ggf. Änderungen vornehmen und abschließend kompilieren und als neue App installieren.
Hier dazu meine Screenshots des Browsers, zunächst die Designer-Ansicht:
Und die Blocks-Ansicht aus zwei Bildern zusammengesetzt:
Nach dem Programmstart des Pico W und Drücken der Schaltfläche Start Scanning in der App sollte der Pico W als BLE-Partner angezeigt werden und die Relais einzeln ein- bzw. ausgeschaltet werden können.
Noch einmal zur Erklärung des Codes: In der App habe ich die Relais der Einfachheit halber von 1 bis 8 nummeriert. Für die Wertigkeit und das Berechnen des Codes muss diese Zahl jeweils um 1 vermindert werden. Auf dem Bild oben links angezeigt der Code 176 = 2 hoch 4 plus 2 hoch 5 plus 2 hoch 7, also Relais 5, 6 und 8 werden geschaltet. Diese Spitzfindigkeit des Programmierers braucht der Anwender nicht zu berücksichtigen.
Sie können selbstverständlich auch kleinere Relais-Module verwenden und die nicht benötigten Code-Anteile im MicroPython-Programm und in der App löschen. Viel Spaß beim Nachbauen und Schalten Ihrer Weihnachtsdekoration.
Wir wünschen eine frohe und gesegnete Weihnacht.
Sollten Sie an dieser Stelle kein Video sehen, müssen Sie Cookies in den Browsereinstellungen zulassen.
]]>In diesem Projekt werden wir ganz besondere, farbenfrohe und auffällige Geschenkboxen herstellen. Schachteln, die die Aufmerksamkeit auf sich ziehen und die Farbe wechseln.
Die drei LED-Panels werden mit den digitalen Pins 6, 7 und 8 des AZ-Mega2560 Mikrocontrollers verbunden. Die für die Versorgung der LED-Panels benötigte 5-VDC-Spannung muss von einem Netzteil mit ausreichender Leistung geliefert werden. Auch der Mikrocontroller wird von der externen 5-VDC-Quelle versorgt. Entweder wie hier am USB-Port, die externe Buchse, oder den Vin Pin. Die Schaltung für dieses Projekt ist sehr einfach, da nur drei LED-Panels und der Mikrocontroller benötigt werden.
Für den Bau der Schachteln werden die Rahmen der Würfel aus den quadratischen Stäben gebaut. Für die kleine Schachtel werden 12 Stäbe von 10 cm Länge, für die mittlere Schachtel 12 Stäbe von 17 cm Länge und für die große Schachtel 12 Stäbe von 22 cm Länge abgeschnitten. Die Ecken der Hölzchen werden zusammengeklebt.
Für die Wände der Schachteln verwenden wir weißes DIN-A4-Papier für die mittlere und die kleine Schachtel und ebenfalls weißes DIN-A3-Papier für die große Schachtel. Das Papier ist leicht lichtdurchlässig und sorgt dafür, das Licht der LEDs zu streuen und homogen zu machen. Für den Boden der Boxen wird Balsa- oder Pappelsperrholz in der gleichen Größe wie die Seiten der Boxen verwendet. Darauf werden die LED-Module installiert.
Die Schachteln ändern die von uns festgelegten Farben und im von uns festgelegten Intervall. Der Quellcode ist im Sketch genau beschrieben.
Es werden die drei Bibliotheken FastLED, Adafruit_NeoMatrix und Adafruit_Neopixel benötigt, um die LEDs zu steuern.
#include "FastLED.h" #include <Adafruit_NeoMatrix.h> #include <Adafruit_NeoPixel.h>
Als Nächstes werden die Anzahl der LEDs und die verwendeten Pins als Konstanten definiert.
#define num_leds_big_box 256 #define pin_big_box 7 #define pin_medium_sized_box 8 #define num_leds_small_box 64 #define pin_small_box 6
Adafruit_NeoMatrix big_box_matrix = Adafruit_NeoMatrix(16, 16, pin_big_box, NEO_MATRIX_BOTTOM + NEO_MATRIX_RIGHT + NEO_MATRIX_COLUMNS + NEO_MATRIX_ZIGZAG, NEO_GRB + NEO_KHZ800); Adafruit_NeoMatrix medium_sized_box_matrix = Adafruit_NeoMatrix(16, 16, pin_medium_sized_box, NEO_MATRIX_BOTTOM + NEO_MATRIX_RIGHT + NEO_MATRIX_COLUMNS + NEO_MATRIX_ZIGZAG, NEO_GRB + NEO_KHZ800); Adafruit_NeoMatrix small_box_matrix = Adafruit_NeoMatrix(8, 8, pin_small_box, NEO_MATRIX_TOP + NEO_MATRIX_RIGHT + NEO_MATRIX_COLUMNS + NEO_MATRIX_PROGRESSIVE, NEO_GRB + NEO_KHZ800);
Nach der Implementierung der Bibliotheken und der Definition der Variablen müssen die LED-Panels initialisiert werden. Dies geschieht in der setup()-Methode, in der auch der anfänglichen Zustand der LEDs konfiguriert wird.
big_box_matrix.begin(); medium_sized_box_matrix.begin(); small_box_matrix.begin(); big_box_matrix.clear(); medium_sized_box_matrix.clear(); small_box_matrix.clear();
In der fortlaufenden loop()-Methode werden die Methoden aufgerufen, die für die Beleuchtung zuständig sind. Sie funktionieren alle gleich, bis auf eine 50ms-Pause, daher beschreibe ich nur eine der Methoden.
Zuerst wird die Beleuchtung für den großen Kasten aufgerufen.
big_box_matrix_red();
Bei der Ausführung dieser Methode wird zunächst die Helligkeit der LEDs auf die Hälfte des Maximalwerts (125 von 254) eingestellt. Dann wird eine Schleife ausgeführt, die von der ersten LED an der Position 0 bis zur letzten an der Position 255 (insgesamt 256 LEDs) läuft, diese auf Rot setzt und einschaltet. Nach dem Einschalten aller LEDs wird eine Pause von 3 Sekunden eingefügt, bevor zur Methode loop() zurückgekehrt und die nächste Zeile ausgeführt wird. in diesem Fall der Aufruf der Methode medium_sized_box_matrix_yellow(), also die mittlere Box. Die Ausführung dieser Methode ist ähnlich wie die oben beschriebene, nur die Farbe und die Box, an der die Änderung vorgenommen werden soll, ändern sich. Eine Liste der "predefined colors" finden Sie hier in der Beschreibung der FastLED-Bibliothek.
void big_box_matrix_red() { big_box_matrix.setBrightness(125); for (int i=0; i<num_leds_big_box; i++) { big_box_matrix.setPixelColor(i, CRGB::Red); big_box_matrix.show(); } delay(3000); }
Die einzige Änderung in der Methode zum Ändern der Farbe des kleinen Kastens ist eine Verzögerung von 50ms innerhalb der for-Schleife.
void small_box_matrix_red() { small_box_matrix.setBrightness(125); for (int i=0; i<num_leds_small_box; i++) { small_box_matrix.setPixelColor(i, CRGB::Red); small_box_matrix.show(); delay(50); } delay(3000); }
Die verschiedenen Methoden lassen die Geschenkkästchen jeweils in unterschiedlichen Farben leuchten.
Wir hoffen, dass Ihnen dieses Weihnachtsdekorationsprojekt gefallen hat und wünschen Ihnen ein frohes Weihnachtsfest.
wenn Sie das Video nicht sehen, müssen Sie Cookies in Ihrem Browser zulassen
]]>Um eine besondere Form, bei der Übermittlung von Weihnachtsgrüßen geht es in diesem Beitrag. Zum Einsatz kommen acht LEDs, die von einem ESP32/ESP8266 über einen Porterweiterungsbaustein PCF8574(A) angesteuert werden. Die LEDs werden in einer Linie auf einer Lochrasterplatine zusammen mit dem PCF8574 platziert.
Die benötigten Bitmuster für die Buchstaben der Nachricht bringen die LEDs zum Leuchten. Dafür sorgt der Controller. Und damit der weiß, wann er mit dem Job anfangen soll, Muster für Muster zum PCF8574 zu übertragen, spendieren wir ihm noch ein Accelerator-Modul (Beschleunigungssensor).
Durch die Bewegung der Schaltung entstehen dann, weil die Muster an wechselnden Orten auftreten, quasi freischwebend die Buchstaben der Nachricht im Raum, der am besten abgedunkelt sein sollte. Wie das Zusammenspiel funktioniert, das erzähle ich Ihnen im heutigen Beitrag aus der Reihe
heute
Die Aufgabe der Schaltung und des MicroPython-Programms, das wir dazu entwerfen werden, habe ich schon beschrieben. Wie lässt sich der Plan umsetzen und warum habe ich die Bauteile ausgewählt? Untersuchen wir die Hintergründe.
Ich beginne mit der Definition eines Buchstabens. Für die Festlegung des Musters habe ich mit Hilfe eines Zeichenprogramms (zum Beispiel Libre Office Draw) ein Raster erstellt und ausgedruckt. Eine Seite mit 8 mal 7 Blöcken können Sie hier herunterladen. Schauen wir uns an, wie der Buchstabe „A“ codiert wird.
Abbildung 1: Buchstabenmatrix
Mit einem Stift wurden auf dem Ausdruck die Felder markiert, die später aufleuchten sollen. Wie die Zeichen aussehen sollen, legen Sie also selbst fest. Auch die Breite, ursprünglich auf sieben Spalten festgelegt, kann verändert werden. Aus einem Zeichensatz mit fester Breite wird so ein Proportional-Zeichensatz. Das „I“ ist dann zum Beispiel schmaler als ein „W“. Das Programm wird das berücksichtigen und zwar auf ganz simple Weise.
So wie in der Abbildung 1 die Spaltenmuster räumlich aufeinander folgen, folgen die Lichtmuster der LED-Zeile zeitlich und damit natürlich ebenfalls räumlich versetzt, wenn man den gesamten Aufbau seitlich bewegt. Wir machen uns später dazu noch weitere Gedanken. Abbildung2 zeigt eine schwankende Hin- und Her- Bewegung.
Abbildung 2: Freischwebender Grußtext
Beginnen wir mit der Besprechung der Hardware.
Welcher Controller eingesetzt wird, steht zur freien Auswahl. Für die Programmentwicklung habe ich immer gerne ein Modul mit Flashtaste, weil ich dadurch die Möglichkeit erhalte, ein Programm geordnet verlassen zu können. Wenn der Umfang des Projekts es erlaubt, setzte ich auch gerne Controller-Boards ein, die auf dem Breadboard noch jeweils eine Kontaktreihe für Jumperkabel freilassen. Deswegen habe ich bei der Entwicklung der Schaltung hier einen ESP8266 Amica verwendet, der hat nämlich außerdem eine Flashtaste an Bord.
Abbildung 3: ESP8266 und GY-271
2 |
D1 Mini NodeMcu mit ESP8266-12F WLAN Modul oder D1 Mini V3 NodeMCU mit ESP8266-12F oder NodeMCU Lua Amica Modul V2 ESP8266 ESP-12F WIFI oder NodeMCU Lua Lolin V3 Module ESP8266 ESP-12F WIFI oder ESP32 Dev Kit C unverlötet oder ESP32 Dev Kit C V4 unverlötet oder ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder NodeMCU-ESP-32S-Kit oder |
1 |
|
8 |
LEDs 3mm Ø z. B. aus LED Leuchtdioden Sortiment Kit, 350 Stück, 3mm & 5mm, 5 Farben oder Jumbo-LEDs 8mm Ø *** ***** |
8 |
Widerstände z. B. aus Widerstände Resistor Kit 525 Stück Widerstand Sortiment**** |
8 |
Widerstände 47Ω***** |
8 |
Widerstände 10kΩ***** |
8 |
PNP-Transistoren BC558 o. ä. ***** |
1 |
IC-Fassung 16-polig |
1 |
Elektrolytkondensator 470µF / 16V z. B. aus ElKo Sortiment Elektrolytkondensator ***** |
1 |
PCF8574-Modul oder |
1 |
|
1 |
|
diverse |
Jumper Wire Kabel 3 x 40 STK. je 20 cm M2M/ F2M / F2F evtl. auch |
optional |
* Für diese Variante gibt es Platinen-Layouts zum Download
** Die Mini-Ausführung der Schaltung kann anhand des Layouts auch leicht auf einer Lochrasterplatine verdrahtet werden.
*** Nur für das große Platinenlayout verwendbar.
**** Die Widerstandswerte richten sich nach der verwendeten LED-Farbe (siehe Text)
***** Für die Bestückung des großen Layouts
Die Schaltung kann in verschiedenen Varianten aufgebaut werden. Das beginnt bei der Auswahl der Platinengröße. Es gibt ein Layout für Jumbo-LEDs mit 8mm Durchmesser und eine Minivariante mit 3mm-LEDs. Die kleine Variante kann auch auf den verlinkten Lochrasterplatinen, dem Layout folgend, bestückt und verdrahtet werden. Wie Sie selbst PCBs über das Bügel- oder Lichtpaus-Verfahren herstellen können ist hier beschrieben. Die PDF-Datei mit den Layouts laden Sie über den Link herunter. Die ausgedruckten Vorlagen werden mit der Druckseite direkt auf die Platine gelegt. Das Duo kommt dann unter das Bügeleisen oder auf die UV-Licht-Box.
Abbildung 4: Schaltung mit Jumbo-LEDs
Abbildung 5: Schaltung Mini-Ausführung
Hier ist der Schaltplan für die etwas komplexere Schaltung mit den Jumbo-LEDs. Um größere Helligkeit zu erreichen, sind die LEDs an eine 5V-Quelle angeschlossen, die auch den Controller, einen ESP8266 D1 mini, mitversorgt. Der Plan zeigt auch den Anschluss des GY-521-Moduls und eines PCF8574-Moduls. Bei der Verdrahtung der Miniversion folgen Sie bitte einfach dem Layout in Abbildung 5. Die Kathode einer LED ist übrigens immer der kürzere Anschluss (kurz wie Kathode). Außerdem ist der Sockelring des Gehäuses auf der Kathodenseite abgeflacht.
Abbildung 6: LED-Zeile - Schaltplan mit PCF8574-Modul
Abbildung 7: LED-Zeile maxi
Was leistet das PCF8574-Modul? Nun, es sorgt dafür, dass alle LEDs der Spalte gleichzeitig aufleuchten. Würde ich die LEDs über separate GPIOs schalten, müsste das zwangsweise zeitversetzt passieren, was zu einer Verformung der Buchstaben führen würde. Mit dem Porterweiterungsbaustein, der sein Bitmuster für die Ausgänge über den I2C-Bus erhält und erst danach parallel umsetzt, werden alle relevanten Positionen simultan geschaltet. An einem ATmega328 gibt es Anweisungen, die alle erreichbaren Bits eines Ports gemeinsam ansteuern. Die ESPs haben diese Möglichkeit leider nicht. Daher der Umweg über den PCF8574.
Wird statt des PCF8574-Moduls wie in Abbildung 6 dargestellt, ein PCF8574-IC verwendet, müssen wir berücksichtigen, dass die I2C-Bus-Leitungen mit je einem Pullup-Widerstand von 4,7kΩ bis 10k auf Vcc gezogen werden müssen. In I2C-Modulen sind diese in der Regel bereits eingebaut. Zusammen mit dem GY521-Modul am Bus sollte es also auch ohne die beiden Pullups funktionieren, es reicht nämlich, wenn an einer Stelle die Widerstände eingefügt sind. Die INT-Leitung wird in diesem Projekt nicht verwendet.
Abbildung 8: SDA-SCL-Beschaltung
Abbildung 9: LED-Zeile mini (mit gelben 3mm-LEDs)
Die Ausgänge des PCF8574 sind durch eine Gegentakt-CMOS-Stufe realisiert. Der untere Transistor zieht den Ausgang auf GND-Potenzial, wenn wir eine 0 an diese Stufe senden. Eine 1 legt den Ausgang über die 100µA-Konstantstromquelle auf Vcc-Potenzial. Das heißt aber auch, dass eine Stromsenke am Ausgang Px maximal 100µA aus der Leitung ziehen kann. Das ist zu wenig, um damit eine nachfolgende Transistorstufe anzusteuern und reicht schon gar nicht, um damit eine LED zu betreiben.
Abbildung 10: Innenleben des PCF8574
Aus diesem Grund sind die Transistoren in Abbildung 5 vom Typ PNP. Damit der BC558 durchschaltet, muss seine Basis auf GND-Potenzial gezogen werden. Das kann der Ausgang des PCF8574 leisten und er kann auch den Kollektor der LEDs in der Mini-Schaltung auf Masse ziehen und dabei bis maximal 25mA aufnehmen, die von der LED geliefert werden. In der vorliegenden Schaltung erlauben die 47Ohm-Widerstände einen Strom von ca. 11 mA. Der BC558-Basisstrom liefert zusammen mit den 100 Ohm- und 10kΩ-Widerständen ca. 2,7mA. Beides ist also im grünen Bereich und kann vom PCF8574-Ausgang aufgenommen und nach GND abgeführt werden.
Abbildung 11: LED direkt am PCF8574
Nun ergibt sich daraus das Problem, dass in den Character-Definitionen eine 1 für eine leuchtende LED steht, der PCF8574 aber eine 0 an dem entsprechenden Ausgang liefern muss. Der Controller muss also den Bytewert vor der Ausgabe an den PCF8574 bitweise negieren. Das passiert im Programmcode durch Exoderieren (XODER) mit 0xFF – alle Probleme gelöst – FAST!
Da ist noch die Sache mit den Strombegrenzungswiderständen, die mit den LEDs in Reihe geschaltet sind und je nach LED-Farbe angepasst werden müssen. Die Vorwärts- oder Durchlassspannung der LED bestimmt zusammen mit der angepeilten Stromstärke letztlich den Wert des Reihenwiderstands. Die Vorwärtsspannung hängt von der Farbe der LED ab und ist dem Datenblatt zu entnehmen. Ganz grob ergibt sich folgender Zusammenhang.
Farbe Uv
rot 1,8V
gelb 1,9V
grün 2,1V
blau 2,8V
violett 3,2V
weiß 3,2V
Am Widerstand muss so viel Spannung abfallen, dass von der Betriebsspannung Vcc an der LED genau die Vorwärtsspannung überbleibt. Das Rechenbeispiel zeigt, wie das geht.
Vcc = 3,3V
LED-Farbe: gelb
UV = 1,9V
UR = 3,3V – 1,9V = 1,4V
Ziel-Stromstärke IZ = 8mA = 0,008A
Widerstandsformel: R = U / I = 1,4V / 0,008A = 175 V/A = 175 Ω
In der E12-er Reihe sind die entsprechenden Werte 180Ω oder, wenn's a Bisserl mehr an Strom sein darf, 150Ω, das ergibt gute 9mA.
Thonny oder
mpu6050.py: Treiber für das GY-521-Modul
shakingtext.py: Demosoftware
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 18.06.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Für den Fall, dass Sie die Schaltung erst einmal ausprobieren wollen, empfiehlt sich der Aufbau auf einem Breadboard. Das oben verlinkte bietet reichlich Platz für alle Bauteile und wir können mit der Besprechung des erfrischend kleinen Programms beginnen.
Wie eingangs schon erwähnt, arbeitet das Programm automatisch auf beiden Controller-Familien. Ich habe hier ein ESP8266 - Amica-Board eingesetzt, weil ich dadurch mit nur einem Breadboard auskommen kann. Weil ich die PCBs für die LEDs aber bereits für Testzwecke hergestellt hatte, stecken auf dem Breadboard nur der ESP8266 D1 mini und das GY-271-Modul. Batterie, Breadboard und LED-PCB habe ich auf eine Sperrholzplatte geschraubt, damit ich alle Teile zusammen auch gut schütteln kann und nichts fliegen geht. Für die Schüttelversuche sollte man außerdem den Aufbau vom USB abtrennen, damit bei den heftigen Bewegungen die Buchse am Controller nicht beschädigt wird. Das Programm wird unter dem Namen main.py, wie oben beschrieben, in den Flash des ESP8266 hochgeladen. Nach einem Reset startet der Controller dann autonom auch ohne USB-Verbindung.
Das Importgeschäft für unser Programm fällt schon mal sehr übersichtlich aus. In der letzten Zeile importieren wir aus dem Modul mpu6050 die Klasse ACCEL. Die Datei mpu6050.py muss sich dafür im Flash des Controllers befinden. Die anderen Klassen und Methoden sind bereits im Kern von MicroPython enthalten.
from machine import SoftI2C,Pin
from time import sleep_ms
import sys
from mpu6050 import ACCEL
Dann fällt die Auswahl des Controllertyps an. Die Textkonstante sys.platform informiert uns über den Controllertyp. Entsprechend den Standardvorgaben instanziieren wir ein I2C-Bus-Objekt. Die für den Bus, laut Datenblatt des PCF8574, vorgegebene Transferfrequenz von 100000kHz bremst den MPU6050 aus, der mit 400000kHz arbeiten könnte.
if sys.platform == "esp8266":
i2c=SoftI2C(scl=Pin(5),sda=Pin(4), freq=100000)
elif sys.platform == "esp32":
i2c=SoftI2C(scl=Pin(22),sda=Pin(21) ), freq=100000)
else:
raise RuntimeError("Unknown Port")
Das I2C-Objekt übergeben wir dem Konstruktor der MPU6050-Klasse und erhalten eine solche Instanz zurück. Dann steuern wir zur flüssigen Unterhaltung zwischen dem Controller und der Peripherie bei, indem wir die Geräteadressen von PCF8574 und MPU6050 deklarieren.
mpu=ACCEL(i2c)
HWADRpcf=0x38
HWADRmpu=0x68
Das Tasten-Objekt für den kontrollierten Programmabbruch und ein Ausgang zum Messen der Frequenz für die Spaltenübertragung der Zeichen werden festgelegt. Als Taste nehme ich die Flash-Taste des ESP8266-Moduls.
taste=Pin(0,Pin.IN,Pin.PULL_UP)
trigger=Pin(14,Pin.OUT, value=0)
Die Zeichen-Matrix legen wir in Form eines Dictionarys fest. In den geschweiften Klammern werden Schlüssel-Wert-Paare, durch Kommas getrennt, aufgelistet. Der Key ist ein ASCII-Zeichen, nach einem Doppelpunkt folgt der zugewiesene Wert in Form eines Tupels. Wie die Feldwerte der Matrix ermittelt werden, habe ich oben schon beschrieben. Die letzte Spalte, die letzte Hexadezimalzahl, ist stets 0x00, damit die Zeichen nicht unmittelbar zusammenkleben wie die Kletten. Fügen Sie gerne noch weitere Trennspalten hinzu, ganz nach Ihrem Gefühl.
zeichen={
"F":(0xff,0x90,0x90,0x90,0x80,0x80,0x00),
"R":(0xff,0x90,0x90,0x90,0x9c,0x63,0x00),
"O":(0x7e,0x81,0x81,0x81,0x81,0x7e,0x00),
"H":(0xff,0x10,0x10,0x10,0x10,0xff,0x00),
"A":(0x3f,0x70,0x90,0x90,0x70,0x3f,0x00),
"E":(0xff,0x91,0x91,0x91,0x81,0x81,0x00),
"S":(0x61,0x91,0x91,0x91,0x91,0x8e,0x00),
"T":(0x80,0x80,0xff,0x80,0x80,0x80,0x00),
"W":(0xfe,0x01,0x1e,0x01,0x06,0xf8,0x00),
"I":(0x81,0xff,0x81,0x00),
"N":(0xff,0x40,0x30,0x0c,0x02,0xff,0x00),
"C":(0x7e,0x81,0x81,0x81,0x81,0x42,0x00),
"*":(0x4a,0x2c,0xf8,0x1e,0x34,0x52,0x00),
"P":(0xff,0x90,0x90,0x90,0x90,0x60,0x00),
"U":(0xfe,0x01,0x01,0x01,0x01,0xfe,0x00),
"J":(0x84,0x82,0x81,0x81,0x82,0xfc,0x00),
" ":(0x00,0x00,0x00,0x00,0x00,0x00,0x00),
}
Es folgen Definitionen für Funktionen zum Zeichentransfer, writeReg() schickt ein Byte an ein Peripherie-Device auf dem I2C-Bus. Zu übergeben sind die Hardware- oder Geräteadresse, die Registeradresse und das Datenbyte.
def writeReg(hwadr, adr, dat):
i2c.writeto(hwadr,bytearray([adr,dat]))
Wichtig ist die Umwandlung von Registeradresse und Bytewert in ein Bytearray. Das ist notwendig, weil die Methode writeto() der i2c-Instanz keine Ganzzahlen, sondern nur Objekte akzeptiert, die das sogenannte Bufferprotokoll unterstützen, das sind Objekte vom Typ bytes- oder bytearray. Wir bauen die beiden Ganzzahlen dazu in eine Liste ein, die wir anschließend in ein Bytearray umwandeln lassen, indem wir die Liste dem Konstruktor der Klasse bytearray übergeben.
Beachten Sie bitte, dass der folgende Befehl ein ganz anderes Ergebnis bringt.
>>> bytearray(2,3)
bytearray(b'\x00\x00')
Hier wird ein Bytearray mit zwei Feldern mit dem Defaultwert 0x00 erzeugt und die 3 wird einfach kommentarlos vom Konstruktor gefressen, wohl bekomm's!
Der PCF8574 ist ein ganz genügsames Kerlchen, das nicht einmal Register besitzt. Schreibanweisungen gehen direkt in den Ausgangspuffer. Das heißt es muss, oder besser darf, keine Registeradresse gesendet werden. pcfWrite() berücksichtigt das.
def pcfWrite(hwadr, dat):
i2c.writeto(hwadr,bytearray([dat]))
Die Hardwareadresse des Chips wird dennoch mit übergeben, denn es können bis zu acht PCF8574 auf dem Bus liegen. Möglich wird das durch die drei Adress-Pins A0, A1 und A2. Für unser Projekt liegen alle drei Pins auf GND-Potenzial. Damit wird der PCF8574 mit seiner Basisadresse angesprochen. Aber selbst die kann variieren, denn es gibt verschiedene Ausführungen dieses Bausteins. Am besten wird es sein, wenn Sie den PCF8574 alleine auf den Bus legen und mit i2c.scan() seine Adresse ermitteln. Das geht in REPL so:
>>>from machine import SoftI2C,Pin
>>> i2c=SoftI2C(scl=Pin(5),sda=Pin(4), freq=100000)
>>> hex(i2c.scan()[0])
'0x38'
Mein PCF8574AP hat demnach die Hardware-Adresse 0x38, in den Datenblättern findet man für den PCF8574 die 0x40 und für den PCF8574A die 0x70. Das Mysterium wird komplett, weil uns i2c.scan() die 7-Bit-Adresse liefert und im Datenblatt aber die 8-BitAdresse mit dem R/W-Bit angegeben ist. Auch für i2c.writeto() und i2c.readfrom() wird die 7-Biteadresse benötigt, weil MicroPython das R/W-Bit, je nach Operation, automatisch selbst hinzufügt. Die Werte aus den Datenblättern müssen also um ein Bit nach rechts geschoben werden.
>>> hex(0x40 >> 1)
'0x20'
>>> hex(0x70 >> 1)
'0x38'
Der Scan hat den PCF8574AP also korrekt als A-Typ geortet.
Um ein Byte vom Bus zu lesen, müssen für einen Baustein, der Register benutzt, Hardware-Adresse und Register-Adresse übergeben werden. Beides wird an den externen Chip gesandt.
readfrom() liest daraufhin ein bytes-Objekt der Länge 1 ein. Durch Indizieren mit [0] erhalten wir den Dezimalwert dieses ersten Bytes.
def readReg(hwadr, adr):
i2c.writeto(hwadr,bytearray([adr]))
return self.i2c.readfrom(hwadr,1)[0]
Es folgt eine Funktion, welche die Bit-Matrix für ein Zeichen an den PCF8574 senden kann.
def showChar(char,delay=5):
c=(0x10,0x10,0x10,0x10,0x10,0x10,0x00)
if char in zeichen:
c=zeichen[char]
for val in c:
trigger.value(1)
val ^= 0xFF
pcfWrite(HWADRpcf,val)
sleep_ms(delay)
trigger.value(0)
showChar() nimmt das ASCII-Zeichen und optional im Schlüsselwortparameter delay die Zeit der Anzeige in Millisekunden. Falls ein nichtdefiniertes Zeichen übergeben wurde, setzen wir in c das Muster für einen Bindestrich, "-". Wird das Zeichen als Schlüssel im Dictionary zeichen gefunden, belegen wir c mit dessen Muster.
In c ist jetzt in jedem Fall eine Zeichen-Matrix. In der for-Schleife tasten wir die Spalten ab. Unser Trigger-Ausgang geht auf 1, wir exoderieren den gepflückten Wert in val mit 0xFF und senden das Ergebnis an den PCF8574. Nach delay Millisekunden geht der Trigger-Pin auf 0. Die Schleife sendet so viele Werte, wie im Tupel in c angegeben sind. Dadurch können beliebig breite Muster dargestellt werden. Den Trigger-Ausgang können wir mit einem DSO (Digitales Speicher Oszilloskop) oder dem Logic Analyzer abtasten, um die tatsächliche Zeichen-Folge-Frequenz zu bestimmen.
Der nächste Schritt bringt die Darstellung von kurzen Texten. Die Umsetzung erfolgt analog zu showChar() durch eine Schleife. Der übergebene String in s wird durch die for-Schleife geparst und die vorgefundenen Zeichen wandern zum PCF8574.
def showString(s, delay=5):
for char in s:
showChar(char, delay)
Eine meiner shortest main loops ever bildet den Abschluss des Programms.
while 1:
while abs(mpu.getValue("z")) < 2000:
pass
showString("FROHES FEST",3)
if taste.value() == 0:
showChar(" ",10)
trigger.value(0)
sys.exit()
Die innere while-Schleife wartet darauf, dass der MPU6050 ein Beschleunigungssignal meldet, das den Grenzwert von 2000 übersteigt, sowohl in positive, als auch in negative Achsenrichtung. Welche Achse das bei Ihnen ist, hängt davon ab, wie Sie das GY-271-Modul eingebaut haben. Orientieren Sie sich dazu einfach über den Aufdruck auf dem Break Out Board. Für seitliche Bewegungen ist das bei mir die Z-Achse, denn das Modul steckt senkrecht zum Breadboard.
Abbildung 12: Z-Achse zeigt in Bewegungsrichtung
Dann folgt die Ausgabe des Strings und danach die Tastenabfrage. Ist die in diesem Moment gedrückt, dann machen wir die LEDs aus, stellen den Trigger-Ausgang auf 0 und verlassen das Programm.
Abbildung 13: gesamter Aufbau
Wenn eine Spalte räumlich neben der vorigen angezeigt werden soll, muss eine Mindestgeschwindigkeit eingehalten werden. Für die 8mm-LEDs ist das in meinem Ansatz mit 3ms Leuchtdauer: 8mm / 3ms = 2,67m/s, für die 3mm-LED nur 1m/s. Für die Anzeige des Strings "FROHES FEST" mit 11 • 7 = 77 Spalten ergeben sich schwebende Texte mit 77 • 8mm = 616mm beziehungsweise 231mm. Einen Eindruck davon, wie das aussieht, vermittelt die Abbildung 2.
In Abbildung 2 wird der Text in umgekehrter Buchstabenfolge angezeigt. Finden Sie eine Möglichkeit das zu ändern? Es ist im Prinzip recht einfach. Was liefert der MPU6050 für Werte, wenn die Richtung der Beschleunigung auf die Schaltung umgekehrt wird? Wie müssen Sie darauf im Programm reagieren? Letzten Endes fällt die Änderung dann doch recht umfangreich aus, weil nicht nur die Richtungsänderung der Bewegung, sondern auch das Parsen des Textes und die Darstellung der Spaltenmuster angepasst werden müssen. Viel Erfolg beim Programmieren von "Schütteltext 2.0"!
Und natürlich wünsche ich Ihnen eine schöne Adventszeit!
]]>
1 |
Raspberry Pi Pico oder Pico W |
1 |
|
alt |
|
1 |
|
Jumperkabel, Taster/Button |
|
optional |
Ein Taster (Button) ist ein federbelasteter Schalter, der nach dem Loslassen den Stromkreis wieder öffnet. Es gilt, zwei Besonderheiten zu beachten: Erstens muss ein Kurzschluss in der Schaltung vermieden werden und zweitens können gerade die preiswerten Taster „prellen“ (engl. bounce, bouncing), d.h. statt des sofortigen elektrischen Kontaktes ruft die Betätigung des Schalters kurzzeitig ein mehrfaches Schließen und Öffnen des Kontakts hervor.
Wenn der GPIO Pin als Eingang deklariert wurde, kann man mit dem Programm abfragen, ob ein HIGH-Signal (Spannung ca. 2 - 3,3V) oder ein LOW-Signal (GND=0V bis max. ca. 1,2V) anliegt. Wenn man durch Drücken des Tasters also 3,3V anlegt, liegt eindeutig HIGH an. Wenn man den Taster wieder loslässt, hat man jedoch einen undefinierten Zustand. LOW wird nur erkannt, wenn Verbindung zu GND hergestellt wird. Wenn dann der Button gedrückt wird, hat man jedoch einen satten Kurzschluss und beschädigt den Mikro Controller. Abhilfe schafft hier ein Pull-down-Widerstand von 10 kΩ, der den Pin auf LOW zieht. Beim Drücken des Tasters wird der Zustand am Pin HIGH, es fließt dabei ein geringer Strom durch den Widerstand gegen GND, der im zulässigen Bereich liegt. Es gilt: I = U / R, also 3,3V / 10 kΩ = 0,33 mA.
Das gleiche gilt bei inverser Logik: Der Pin wird dauerhaft über einen Pull-up-Widerstand auf HIGH gehalten und beim Drücken des Tasters auf LOW gezogen. Das Schöne ist, dass die Mikrocontroller einen internen Pull-up-Widerstand besitzen, der im Programm zugeschaltet werden kann. Deshalb wird diese Schaltung heutzutage häufiger angewendet.
Nun zur Programmierung mit dem MicroPython-Modul picozero. Hier gibt es die Klasse Button, die zu Beginn des Programms mit
from picozero import Button
importiert wird, Anschließend wird das Objekt button instanziiert mit z.B.
button = Button(16) # GP16 = phys. Pin 21
In der Klassendefinition erkennt man, dass es zwei optionale Parameter gibt, die voreingestellt sind:
class picozero.Button(pin, pull_up=True, bounce_time=0.02)
Mit diesen Voreinstellungen sind die beiden o.g. Probleme gelöst. Der Button kann gegen GND geschaltet werden und Prellen wird durch eine „bounce_time“ von 20 ms unterdrückt. Selbstverständlich können diese Werte bei der Instanziierung des Objekts geändert werden, z.B.
button = Button(17, pull_up=False, bounce_time=0.01) # GP17 = phys. Pin 22
Zwei einfache Beispielprogramme, das erste sicherlich selbsterklärend: from picozero import Button
import utime as time
button = Button(16)
while True:
if button.is_pressed:
print("Button is pressed")
start = time.ticks_ms()
else:
print("Button is not pressed")
sleep(0.1)
Beim zweiten Beispielprogramm sieht es auf den ersten Blick aus, als würde die Funktion pico_led.on() aufgerufen. Allerdings dürfen bei button.when_pressed=pico_led.on keine Klammern() verwendet werden. Die Dokumentation schreibt: Die Funktion wird nicht aufgerufen, sondern es wird eine Referenz zur Funktion eingerichtet.
from picozero import Button, pico_led # internal LED
button = Button(16) # default: pull_up=True, bounce_time=0.02
button.when_pressed = pico_led.on # Remark: no brackets ()
button.when_released = pico_led.off
Etwas aufwendiger ist das Programm mit dem MicoPython-Modul machine, das jedoch ggf. auch bei anderen Mikrocontrollern wie ESP32 verwendet werden kann.
from machine import Pin
import utime as time
button = Pin(16, Pin.IN, Pin.PULL_UP)
reset = Pin(17, Pin.IN, Pin.PULL_UP)
# Codezeile für Raspberry Pi Pico (ohne WLAN)
led_onboard = Pin(25, machine.Pin.OUT)
# Codezeile für Raspberry Pi Pico W (mit WLAN)
#led_onboard = Pin("LED", machine.Pin.OUT)
while True:
if button.value() == 0:
print("button is pressed")
led_onboard.on()
if reset.value() == 0:
print("reset is pressed")
led_onboard.off()
time.sleep(0.1)
Im Vorgriff auf den nächsten Abschnitt Sensoren, die meist nur einen kurzen Schaltimpuls erzeugen, habe ich bereits einen zweiten Taster zum Ausschalten mit Namen reset eingeführt.
Nun zu den Sensoren, die wie Schalter oder Taster funktionieren; zunächst die rein digitalen Sensoren: (Gabel-)Lichtschranke, Familie der Schüttelsensoren (auch Schock-, Klopf-), Bewegungsmelder, Magnetschalter, Digitaler Hallsensor, Flammensensor. (Die Zahlen beziehen sich auf die Abschnitte im eBook für das Sensor Kit am ATmega Mikrocontroller.)
from picozero import pico_led, Button
import utime as time
button = Button(16)
reset = Button(17)
while True:
button.when_pressed = pico_led.on
reset.when_pressed = pico_led.off
Die eingebaute LED pico_led braucht nicht instanziiert werden. Wer eine externe LED oder einen Buzzer anschließen möchte, muss diese Klassen importieren und die Objekte mit der gewünschten Pin-Nummer instanziieren (siehe Teil 1).
Bevor ich die Sensoren mit vier Anschlüssen, neben + und GND häufig D0 und A0 (für digitaler und analoger Ausgang) anspreche, möchte ich zunächst die analogen Eingänge des Pico beschreiben.
Analoger Eingang bedeutet, dass nicht nur Low oder High erkannt wird, sondern eine Spannung zwischen Null und der Referenzspannung (beim Pico 3,3V, sofern am Pin 35 (=ADC_VREF) keine andere Spannung angelegt wird) in einen Zahlenwert umgewandelt wird. Die Auflösung liegt beim Pico bei 12-Bit, also grundsätzlich einem Wert zwischen 0 und 4095, dieser wird jedoch mit der Methode adc.read_u16() aus dem Modul machine skaliert auf den Bereich 0 bis 65535, bei dem Modul picozero normiert auf den Bereich 0 bis 1. Zum Vergleich beim Uno ist die Auflösung 10-Bit, das ergibt einen Wert zwischen 0 und 1023.
Der RP2040 hat grundsätzlich fünf ADC-Kanäle, von denen beim Raspberry Pi Pico drei voll nutzbar sind. ADC0 liegt an GP26 (phys. Pin 31), ADC1 an GP27 (phys. Pin 32) und ADC2 an GP28 (phys. Pin 34); ADC3 ist beim Pico mit VSYS verbunden und ADC4 mit dem internen Temperatursensor, der allerdings wenig geeignet ist für die Messung der Umgebungstemperatur.
Hier Beispielcode für das Modul machine und anschließend Modul picozero.
N.B. Grundsätzlich kennt das Modul machine auch die Methode adc.read_uv(), also das Erkennen der Spannung. Das funktioniert jedoch nicht beim Pico.
from machine import ADC, Pin
import utime as time
adc = ADC(Pin(26)) # create ADC object on ADC pin
while True:
value = adc.read_u16() # read value, 0-65535 across voltage range 0.0v - 3.3v
print("value = ",value)
time.sleep(0.1)
# Potentiometer connected to GP26 (ADC0), GND and 3V3
from time import sleep
from picozero import Pot
pot = Pot(26)
while True:
print(pot.value, pot.voltage)
sleep(0.1)
Das Joystick-Modul besteht aus zwei Potentiometern und einem Taster. Hier ein Programmbeispiel:
# Joystick module connected to GP26 (ADC0), GP27 (ADC1), GP16 (button), GND and 3V3
from time import sleep
from picozero import Pot, Button
potx = Pot(26)
poty = Pot(27)
button = Button(16)
while True:
x = potx.value
y = poty.value
b = button.value
print("x = ", x, " y = ", y, "b = ", b)
sleep(0.1)
Bitte beachten: Auch hier werden nach den Aufrufen von .value keine Klammern gesetzt. Ansonsten erhält man die Fehlermeldung „… is not callable“.
Zwei weitere Sensoren liefern analoge Werte und können wie ein Potentiometer angeschlossen werden: der Lichtsensor (LDR Widerstand) und der Thermistor.
Ebenso kann man mit logischen Verknüpfungen bei Erkennen einer Bewegung das Licht erst einschalten, wenn gleichzeitig eine gewisse Lichtstärke unterschritten wird.
Nun zu den Sensoren mit vier Anschlüssen, außer der Spannungsversorgung einem digitalen und einem analogen Ausgang. Meist haben diese Sensoren auf der Platine noch ein kleines, blaues Trimm-Potentiometer und eine LED.
Wir haben bereits gesehen, wie die digitalen und wie die analogen Signale ausgewertet werden. Macht es also Sinn, beide Signale gleichzeitig auszuwerten? Ja, macht es. Insbesondere im Hinblick auf das eingebaute Potentiometer! Denn mit Hilfe der analogen Werte kann man den Grenzwert am Poti einstellen, bei dem die LED aufleuchtet und der digitale Pin auf High geschaltet wird. Denn die Auswertung des digitalen Signals ist einfacher und in Schaltungen leichter zu realisieren.
Noch ein Wort zu den Mikrofon-Modulen. Damit werden keine Sprache oder Musik aufgezeichnet, es geht nur um den Lärmpegel! Als Anwendung kommt z.B. ein „Klatschschalter“ in Frage, wie er 1959 im Film „Bettgeflüster“ mit Doris Day und Rock Hudson noch ohne Mikrocontroller realisiert wurde.
Bei den letzten beiden Sensoren muss man auf das analoge Signal für die Justierung verzichten, aber auch hier kann die Empfindlichkeit mit Trimm-Potentiometern verändert werden. Da hilft vor allem Ausprobieren.
Zu Beginn werden noch einmal die wichtigsten Angaben zum Raspberry Pi Pico bzw. Pico W und zur Programmierumgebung Thonny zusammengefasst, um dann wie traditionell üblich mit dem „Hello World“ des „Physical Computing“ - dem Blinken einer LED - zu beginnen. Genau wie die LEDs werden auch einige andere Bauteile des Kits an GPIOs (General Purpose Input/Output), also Pins als Ausgänge angesteuert, so dass diese ebenfalls im ersten Teil behandelt werden.
Im zweiten Teil werden die GPIOs als Eingänge benutzt, zunächst am Beispiel des Tasters, um die Bedeutung von Pull-up- bzw. Pull-down-Widerständen zu verstehen. Dann werden die Sensoren behandelt, die wie Schalter/Taster funktionieren. Da einige der Sensoren sowohl digitale als auch analoge Ausgänge besitzen, werden auch die anlogen Eingänge mit ihren Besonderheiten thematisiert.
Der dritte Teil widmet sich dann den Sensoren, die eine besondere elektronische Schnittstelle benötigen, One-Wire-Interface, I2C (=Inter Integrated Circuit Interface) und SPI (=Serial Peripheral Interface). Hier werden auch weitere Sensoren aus dem Sortiment von AZ-Delivery behandelt, die nicht Teil des Kits sind.
1 |
Raspberry Pi Pico oder Pico W |
1 |
|
alt |
|
1 |
|
Jumperkabel, Taster/Button |
|
optional |
Zunächst noch einmal das Pinout-Diagramm des Pico W. Dabei ist die Belegung der nach außen geführten Pins identisch mit dem Pico ohne WiFi/Bluetooth, nur die Debug-Pins liegen an anderer Stelle.
Wie auch bei anderen Mikro Controllern sind viele der GPIOs mehrfach belegt mit besonderen Schnittstellen. Wie angekündigt, wollen wir zunächst die eingebaute LED blinken lassen. Dazu benutzen wir als Programmierumgebung für MicroPython das Programm Thonny. Eine ausführliche Erklärung der Installation von Thonny und erste „Gehversuche“ siehe Blog-Reihe „Raspberry Pi Pico und Thonny mit MicroPython“ (Link zu Teil 1, Teil 2, Teil 3).
Anders als beim ATmega328 Uno kann man keine externe LED zur internen LED parallelschalten, der entsprechende Pin ist nicht herausgeführt. Und es gibt einen Unterschied zwischen dem Pico und dem Pico W: Beim Pico wird GPIO25 benutzt, beim Pico W wird diese Leitung für den Infineon Chip für WiFi/Bluetooth verwendet, an dem dann auch die LED angeschlossen wird.
Es folgt der Code für den Pico. Man importiert das Modul machine und instanziiert das Objekt led_onboard = machine.Pin(25, machine.Pin.OUT).
Auskommentiert ist die Version für den Pico W, in dem anstelle von Pin 25 der String-Term „LED“ verwendet wird.
import machine
import utime as time
# Codezeile für Raspberry Pi Pico (ohne WLAN)
#led_onboard = machine.Pin(25, machine.Pin.OUT)
# Codezeile für Raspberry Pi Pico W (mit WLAN)
led_onboard = machine.Pin("LED", machine.Pin.OUT)
# Einfache Variante mit while True-Schleife
# while True:
# led_onboard.toggle()
# time.sleep(1)
# Variante mit try und except, um die LED bei KeyboardInterrupt auszuschalten
try:
while True:
led_onboard.toggle()
time.sleep(1)
except KeyboardInterrupt:
led_onboard.off()
Mit diesem Code und dem Modul machine können ggf. auch andere Mikrocontroller mit MicroPython programmiert werden.
Aber es gibt speziell für den Raspberry Pi Pico ein MicroPython-Modul, das in Anlehnung an das Python-Modul gpiozero entstanden ist und den Namen picozero trägt. Diese Module sind für mich Paradebeispiele für objektorientiertes Programmieren (OOP); man instanziiert ein Objekt, z.B. led, und wendet darauf die Methoden (so nennt man dann die Funktionen) an, z.B. led.on(). Deshalb verwende ich dieses Modul, wenn die Bauteile bereits implementiert sind.
Man sucht und installiert picozero unter Extras/Verwalte Pakete … (engl. Tools/Manage packages …)
Zunächst werden Teile der Module picozero und time importiert. Das Modul picozero kennt die eingebaute LED als pico_led und unterscheidet dabei nicht die verschiedenen Varianten. Der folgende Code funktioniert also sowohl auf dem Pico als auch dem Pico W.
from picozero import pico_led
from time import sleep
while True:
pico_led.on()
sleep(0.5)
pico_led.off()
sleep(0.5)
Wie gesagt, kann man keine externe LED parallelschalten. Deshalb nehmen wir einen beliebigen anderen GPIO-Pin, z.B. GP15 am physischen Pin 20, dem untersten auf der linken Seite des o.a. Pinout-Diagramms.
Beim Anschluss bitte nicht den Vorwiderstand (220 bis 330 Ω) vergessen, damit die LED keinen Schaden nimmt. Bei den LED-Modulen der Sensor-Kits sind diese Vorwiderstände bereits auf der Mini-Platine angebracht. Beim Bi-color LED-Modul benötigt man zwei GPIOs, beim Traffic Light Modul und der RGBLED benötigt man drei GPIOs plus Masseanschluss (=Ground, GND, - )
Code:
from picozero import LED
from time import sleep
led = LED(15)
while True:
led.on()
sleep(0.5)
led.off()
sleep(0.5)
Code für einen einfacher Ampelzyklus mit dem LED Traffic Light Module
from picozero import LED
from time import sleep
redLed = LED(13)
yellowLed = LED(14)
greenLed = LED(15)
while True:
redLed.on()
sleep(2)
yellowLed.on()
sleep(1)
redLed.off()
yellowLed.off()
greenLed.on()
sleep(2)
greenLed.off()
yellowLed.on()
sleep(1)
yellowLed.off()
Gerade bei den mehrfarbigen LEDs möchte man die einzelnen Farben nicht nur ein- oder ausschalten, sondern auch beliebig mischen. Für das Dimmen der einzelnen LEDs benötigt man die Pulsweiten Modulation (PWM, engl. pulse width modulation). Dafür gibt es zwei Möglichkeiten:
Bei der Klasse LED kann man die Methode brightness mit einem Funktionswert zwischen 0 und 1 anwenden.
Beispiel:
led.brightness = 0.5 # half brightness
Bei der Klasse RGBLED wird als Argument ein Tupel mit 8-Bit-Werten (Zahlen zwischen 0 und 255) für die jeweiligen Farbanteile angegeben.
Beispiel:
from picozero import RGBLED
rgb = RGBLED(red=13, green=14, blue=15) # GPIOs
rgb.color = (255, 127, 0) # full red, half green = yellow
Sie haben eine LED in dem Kit entdeckt, die nicht sichtbar blinkt? Das könnte eine IR-LED, das KY-005 IR Transmitter Module sein. Diese IR-LED sendet nicht sichtbares Licht im Infrarotbereich aus, genauso wie die meisten TV-Fernbedienungen. Als Hilfsmittel empfehle ich hier die Kamera des Smartphones. Auf dem Display des Smartphones wird das IR-Signal angezeigt. Das Gegenstück dazu ist das KY-022 IR Receiver Module, das z.B. auch im Smart Car Kit verwendet wird. Die Anwendungsfälle für solch ein Modul sind sehr umfangreich, weswegen ich darauf hier nicht weiter eingehen werden.
Nun zu den weiteren Bauelementen in unseren Kits, die wie eine LED mit Hilfe eines GP-Ausgangs geschaltet werden: diese sind Relais und Aktive Summer (Active Buzzers).
Relais gehören zu den Bauelementen, die eine externe Spannungsversorgung benötigen. Es gibt also drei Anschlüsse für +, - und S (=Signal). Der Signalpin wird direkt an einem GP-Ausgang angeschlossen, die Spannungsversorgung des Einer-Relais im Kit kann bei Betrieb des Pico am PC an den Pin 40 VBUS angeschlossen werden, GND (-) an einer der vielen GND-Pins (3, 8, 13, 18, 23, 28, 33 oder 38). Besser und bei mehreren Relais notwendig ist eine getrennte Spannungsversorgung z.B. mit Batterien, denn elektromagnetische Verbraucher (Relais, Motoren) haben physikalische Eigenschaften, die sich nicht gut mit empfindlicher Elektronik vertragen. Wichtig ist, dass die GND-Anschlüsse von Pico und externer Batterie verbunden werden.
Für das Schalten des Relais kann man die Klasse LED und die Methoden on() und off() verwenden. Es kann eine Besonderheit auftreten: Es gibt auch Relais-Module, bei denen der Signalpin auf GND (LOW) geschaltet werden muss, damit das Relais anzieht.
Geschaltet wird in einem galvanisch getrennten Stromkreis, d.h. es gibt keine elektrische Verbindung zur Steuerelektronik. Von den drei Schraubanschlüssen ist der mittlere der Eingang, die beiden Äußeren die Ausgänge mit den Bezeichnungen NO=normally open und NC=normally closed. Das bedeutet, dass beim Anziehen des Relais der Ausgang NC geöffnet und NO geschlossen wird.
Zunächst muss erklärt werden, worin der Unterschied zwischen aktiven und passiven Summern (Buzzers) besteht. Der Aktive Summer hat in seinem Gehäuse einen eigenen Modulator, der den kleinen Lautsprecher laut und vernehmlich, aber stets mit der gleichen Frequenz summen lässt. Man erkennt ihn an den zwei unterschiedlich langen Beinchen (das längere Beinchen ist +, das kürzere -). Häufig befindet sich noch ein kleiner Aufkleber mit + und - und dem Text „remove after washing“ auf der Oberseite über einem kleinen Loch. Gemeint ist hier „nach dem Einlöten“. Empfehlung: Aufkleber nicht entfernen, die Biester sind auch so laut genug bei Schülerversuchen. Diese Art Buzzer findet man im Haushalt in der Waschmaschine, bei LKWs beim Rückwärtsfahren usw.
Beispielcode mit picozero:
# Active Buzzer that plays a note when powered
from time import sleep
from picozero import Buzzer
buzzer = Buzzer(10) # GPIO Pin
buzzer.on()
sleep(1)
buzzer.off()
sleep(1)
buzzer.beep()
sleep(4)
buzzer.off()
Beim passiven Summer wird die Modulation im Mikrocontroller vorgenommen. Das ist aufwendiger, aber ermöglicht auch unterschiedliche Tonhöhen, um z.B. eine Zweitonfanfare zu bauen, oder eine kleine Melodie zu spielen. Das MicroPython-Modul picozero kennt dazu die Klasse Speaker. Mehr dazu in der Dokumentation von picozero (https://picozero.readthedocs.io/en/latest/)
Im zweiten Teil werden wir die GPIOs als Eingänge kennenlernen.
]]>Neben IFTTT, ThingSpeak und Telegram bietet auch WhatsApp die Möglichkeit, mittels Bot Nachrichten zu verschicken. Die verwendeten Module sind nicht sehr anspruchsvoll was den Speicherbedarf angeht. Und so sind unsere Controller ESP32 und ESP8266 wieder beide mit dabei. Die Textnachricht, die wir an CallMeBot senden, muss URL-encoded sein. Das heißt, dass Sonderzeichen wie "äöüß" durch Hexadezimalcodes ersetzt werden müssen. Das macht mein Modul urlencode.py. Dank urequests.py brauchen wir zur Übermittlung auch bei diesem Projekt nur eine Zeile für den Transfer der Daten zum Server. Wie Sie einen WhatsApp-Account und einen Bot einrichten, erfahren Sie in diesem Beitrag aus der Reihe
heute
Vergessen, das Kellerlicht auszuschalten? Ja, das passiert immer mal wieder. Gut, wenn unser ESP das registriert und uns nach einer gewissen Zeit über WhatsApp eine Nachricht zukommen lässt. Der Schaltungsaufwand ist denkbar gering. In der Magerausbaustufe genügt ein Controller, ein LDR (Light dependend resistor = Fotowiderstand) und ein einfacher Widerstand von 10kΩ, oder das Modul KY-018, auf dem beides schon montiert ist. Wer möchte, kann ein Display mit dazu nehmen, damit Systemmeldungen angezeigt werden können. Die Schaltung wird im Einsatz ja sehr wahrscheinlich nicht an den PC angeschlossen sein.
1 |
D1 Mini NodeMcu mit ESP8266-12F WLAN Modul oder D1 Mini V3 NodeMCU mit ESP8266-12F oder NodeMCU Lua Amica Modul V2 ESP8266 ESP-12F WIFI oder NodeMCU Lua Lolin V3 Module ESP8266 ESP-12F WIFI oder ESP32 Dev Kit C unverlötet oder ESP32 Dev Kit C V4 unverlötet oder ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder NodeMCU-ESP-32S-Kit oder |
1 |
|
1 |
KY-018 Foto LDR Widerstand oder Fotowiderstand Photo Resistor plus Widerstand 10 kΩ |
2 |
|
1 |
|
diverse |
Jumper Wire Kabel 3 x 40 STK. je 20 cm M2M/ F2M / F2F evtl. auch |
Damit neben dem Controller noch Steckplätze für die Kabel frei sind, habe ich zwei Breadboards, mit einer Stromschiene dazwischen, zusammengesteckt.
Abbildung 1: Schaltung ESP8266 D1 mini mit diskreten Widerständen
Der LDR verringert bei zunehmender Helligkeit seinen Widerstandswert. Er ist mit einem Festwiderstand von 10kΩ in Reihe geschaltet. Die beiden bilden zusammen einen Spannungsteiler. Am Mittelkontakt S liegen Spannungen von etwas über 0V bis nahe 3,3V an. Damit bei viel Licht auch eine höhere Spannung an S auftritt, muss der LDR an 3,3V und der 10kΩ-Widerstand an GND liegen. Wenn man den losen LDR verwendet, kann man das leicht realisieren.
Abbildung 2: Spannungsteiler aus Einzelteilen
Beim Modul KY-018 liegt der LDR aber gegen Masse, wenn man es so anschließt, wie es die Beschriftung vorgiebt. Damit sich das Teil genauso verhält, wie wir es wünschen, muss man GND an den mittleren Stift legen und +3,3V an den rechten. S verbinden wir mit dem Analogeingang des Controllers, GPIO36 beim ESP32 und A0 beim ESP8266.
Abbildung 3: Schaltung des Moduls KY-018
Abbildung 4: Schaltung mit ESP32 und LDR-Modul
Abbildung 5: Schaltung mit ESP8266 und Einzelwiderständen
Thonny oder
ssd1306.py Hardwaretreiber für das OLED-Display
oled.py API für das OLED-Display
urequests.py Treibermodul für den HTTP-Betrieb des ESP8266
urlencode.py URL-Encoder-Modul
timeout.py Softwaretimer-Modul
whatsApp.py Demoprogramm für den e-Mailversand
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 18.06.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Für den PC gibt es auch eine Windows-App bei portapps.io. Das Entpacken nach dem Start der heruntergeladenen EXE-Datei läuft reibungslos. Allerdings bringt das Programm nach dem Start eine Fehlermeldung und bricht ab.
Alternativ kann man eine App auch bei Chip, oder im Microsoft Store. Sie arbeitet aber stets mit dem Handy zusammen, die Geräte müssen gekoppelt werden. Nach dem Start der sofort lauffähigen Datei wird folgendes Fenster eingeblendet:
Abbildung 6: WhatsApp portable für den PC
Öffnen Sie WhatsApp auf dem Handy. Gehen Sie ins Menü und tappen Sie Verknüpfte Geräte. Scannen Sie nun den QR-Code mit dem Handy ab. Kurz darauf bekommen Sie ein zweigeteiltes Fenster, in dem Sie links den Chat auswählen. Rechts sehen Sie die Nachrichten.
Abbildung 7: API-Key für den neuen Bot
Abbildung 8: Test-Nachricht vom Bot
Das Importgeschäft ist von etwas größerem Stil. Pin, SoftI2C und ADC kommen vom Modul machine. time liefert sleep. Software-Timer für Microsekunden, Millisekunden und Sekunden, die das Programm nicht blockieren, liegen in timeout bereit. urlencode bietet die Funktion URLEncode(), die, zusammen mit den Listen q und z, spezielle Zeichen in Hexadezimalcode übersetzt.
from machine import Pin, SoftI2C, ADC
from time import sleep
from timeout import *
from urlencode import *
import urequests as requests
from oled import OLED
import network
import sys
import gc
HTTP-Anfragen werden mit urequests sehr vereinfacht. Die Klasse OLED ist die API für das Display, network macht die Verbindung zum WLAN-Router. sys nutzen wir für die Abfrage des Controllertyps und für einen gesicherten Programmausstieg. gc steht für Garbage collection und räumt nicht mehr benötigten Datenmüll weg.
trigger=500 # counts
warnLevel=60 # Sekunden
Der ADC-Level trigger, der hell von dunkel trennt, muss für jeden Fall individuell eingestellt werden, ebenso wie warnLevel, der Wert für die Wiederholung des Nachrichtenversands.
mySSID = 'EMPIRE_OF_ANTS'; myPass = "nightingale"
key="1234567"
phone="+49123456789"
Für mySSID und myPass setzen Sie bitte die Credentials für Ihren Router ein. Gleiches gilt für API-Key und Rufnummer.
Der nächste Block erkennt den Controllertyp und instanziiert dementsprechend ein I2C-Objekt und den ADC.
if sys.platform == "esp8266":
i2c=SoftI2C(scl=Pin(5),sda=Pin(4))
adc=ADC(0)
elif sys.platform == "esp32":
i2c=SoftI2C(scl=Pin(22),sda=Pin(21))
adc=ADC(Pin(36))
adc.atten(ADC.ATTN_11DB)
adc.width(ADC.WIDTH_10BIT)
else:
raise RuntimeError("Unknown Port")
Die I2C-Bus-Instanz übergeben wir an den Konstruktor des Display-Objekts, stellen die Helligkeit ein und geben die Titelzeile aus.
d=OLED(i2c,heightw=64) # 128x64-Pixel-Display
d.contrast(255)
d.writeAt("Kellerlicht",2,0)
Die Taste legen wir an GPIO14 und aktivieren den Pullup-Widerstand.
taste=Pin(14,Pin.IN,Pin.PULL_UP)
Das Dictionary connectStatus übersetzt die Nummern-Codes, die uns network.status() liefert, in Klartext. Der ESP32 liefert andere Nummern als der ESP8266.
connectStatus = {
1000: "STAT_IDLE",
1001: "STAT_CONNECTING",
1010: "STAT_GOT_IP",
202: "STAT_WRONG_PASSWORD",
201: "NO AP FOUND",
5: "UNKNOWN",
0: "STAT_IDLE",
1: "STAT_CONNECTING",
5: "STAT_GOT_IP",
2: "STAT_WRONG_PASSWORD",
3: "NO AP FOUND",
4: "STAT_CONNECT_FAIL",
}
Damit das Station-Interface vom WLAN-Router den Zugang erhält, muss die MAC-Adresse davon dem Router bekannt sein, falls dort MAC-Filterung aktiviert ist. Es ist übrigens keine gute Idee, die Filterung auszuschalten, weil sich dann beliebige WLAN-Nomaden leichter am Router einloggen können.
Die Anmeldesequenz zum Router habe ich dieses Mal in eine Funktion verpackt, die das Station-Objekt zurückgibt. Solange noch keine Verbindung besteht, wird im Sekundentakt ein Punkt im Terminal und am Display ausgegeben.
def connect2router():
# ************** Zum Router verbinden *******************
nic=network.WLAN(network.AP_IF)
nic.active(False)
nic = network.WLAN(network.STA_IF) # erzeugt WiFi-Objekt
nic.active(True) # nic einschalten
sleep(1)
MAC = nic.config('mac')# binaere MAC-Adresse abrufen und
myMac=hexMac(MAC) # in Hexziffernfolge umwandeln
print("STATION MAC: \t"+myMac+"\n") # ausgeben
sleep(1)
if not nic.isconnected():
nic.connect(mySSID, myPass)
print("Status: ", nic.isconnected())
d.writeAt("WLAN connecting",0,1)
points="............"
n=1
while nic.status() != network.STAT_GOT_IP:
print(".",end='')
d.writeAt(points[0:n],0,2)
n+=1
sleep(1)
print("\nStatus: ",connectStatus[nic.status()])
d.clearAll()
STAconf = nic.ifconfig()
print("STA-IP:\t\t",STAconf[0],"\nSTA-NETMASK:\t",\
STAconf[1], "\nSTA-GATEWAY:\t",STAconf[2] ,sep='')
print()
d.writeAt(STAconf[0],0,0)
d.writeAt(STAconf[1],0,1)
d.writeAt(STAconf[2],0,2)
return nic
Weil der ESP8266 bereits beim Einschalten eine Funkverbindung zu dem Router aufbaut, mit dem er schon einmal eine Verbindung hatte, erscheint kein Punkt. Folgender Test bestätigt das. Verbinden Sie einen solchen ESP8266 mit dem PC und starten Sie Thonny. Im Terminal geben Sie folgende Anweisungen ein.
>>> import network
>>> nic = network.WLAN(network.STA_IF)
>>> nic.active(True)
>>> nic.isconnected()
True
Das ist auch der Grund, warum der ESP8266 manchmal ständig neu bootet. Er versucht, den Router zu kontaktieren. Wenn das nicht gelingt, macht er einen Neustart. Mitunter hilft es dann, webrepl (das Funkterminal) auszuschalten.
# Nach dem Flashen der Firmware auf dem ESP8266:
>>> import webrepl_setup
> d fuer disable
# Dann RST; Neustart!
Der ESP32 zeigt dieses merkwürdige Verhalten nicht. Deshalb erscheinen hier auch um die drei bis fünf Punkte.
Um das Rauschen der ADC-Werte zu verringern, ermittelt die Funktion messen() den Mittelwert von n Messungen. Der Rückgabewert ist 1, wenn der Messwert größer als der Grenzwert in trigger ist, sonst 0. Uns interessiert nicht der Quasi-Helligkeitswert, sondern nur ob das Licht an (1) oder aus (0) ist.
def messen(n):
val=0
for _ in range(n):
val+=adc.read()
val//=n
return 1 if val > trigger else 0
Die Hauptschleife wird klarer, wenn man Jobs wie messen() oder Ereignishandler in Funktionen auslagert. Auch das Senden der Nachricht an WhatsApp ist deshalb als Funktion codiert.
def sendeNachricht(text):
url="https://api.callmebot.com/whatsapp.php?phone="+\
phone+"&text="+URLEncode(text)+"&apikey="+key
# print(url)
resp=requests.get(url)
if resp.status_code == 200:
print("Transfer OK")
else:
print("Transfer-Fehler")
resp.close()
gc.collect()
Alles an Information wird in die Variable url verpackt, Protokoll, Server, Rufnummer, der urlcodierte Text und und der API-Schlüssel. Zur Kontrolle kann das Ergebnis ausgegeben werden. Dann schicken wir die URL mit der Methode GET auf die Reise. Das Attribut status_code des Response-Objekts resp sagt uns, ob der Transfer erfolgreich war oder nicht. In jedem Fall schließen wir den Socket und räumen den Speicher auf.
nic=connect2router()
sys.exit()
Die Verbindung wird aufgebaut. Während der Entwicklungsphase brechen wir an dieser Stelle das Programm ab. Wir haben jetzt eine funktionierende Netzwerkverbindung, außerdem sind die ganzen Objekte, Variablen und Funktionen deklariert. So können wir die einzelnen Komponenten händisch testen.
>>> messen(20)
1
Jetzt den LDR abdecken
>>> messen(20)
0
>>> sendeNachricht("Morgenstund ist aller Laster Anfang")
Transfer OK
Ein paar Sekunden später melden sich das Handy und die Windows-App.
Abbildung 9: Erste Nachricht vom ESP32
Wenn alles geklappt hat, kommentieren wir sys.exit() aus.
Der Wecker für den nächsten Scan wird auf 20 Millisekunden gestellt. Dann holen wir den Lichtwert, die Weckzeit für warnen stellen wir auf unendlich.
nextScan=TimeOutMs(20)
alt=messen(10)
warnen=TimeOut(0)
if alt==1:
start=time()
warnen=TimeOut(warnLevel)
Falls das Licht bereits an ist, merken wir uns den Zeitpunkt und stellen den Warn-Timer auf warnlevel. alt ist der Zustand der zurückliegenden Messung.
Dann geht es in die Hauptschleife. Wenn der Timer für die nächste Abtastung des LDR abgelaufen ist, holen wir uns den aktuellen Zustand. Abhängig vom alten und neuen Zustand können drei Situationen entstehen.
Das Licht war aus und ist jetzt an.
Wir stellen den Timer warnen auf warnLevel und merken uns die Zeit. Keine weitere Aktion.
Das Licht war und ist noch immer an.
Ist der Timer warnen() abgelaufen, dann wird es Zeit, eine Nachricht abzufeuern. Wir stellen den Timer warnen erneut auf warnLevel.
Das Licht war an und ist jetzt aus.
Wir nehmen erneut die Zeit und berechnen daraus die Einschaltdauer. Mit einer neuen Nachricht geben wir Entwarnung.
while 1:
if nextScan():
neu=messen(10)
if alt==0 and neu==1:
warnen=TimeOut(warnLevel)
start=time()
elif alt==1 and neu==1:
if warnen():
sendeNachricht("Licht ist seit {} s an.".\
format(warnLevel))
warnen=TimeOut(warnLevel)
elif alt==1 and neu==0:
ende=time()
dauer=ende-start
if dauer > warnLevel:
sendeNachricht("Licht ist nach {} s aus.".\
format(dauer))
In jedem Fall wird der neue Lichtwert auf den alten übertragen. Weil nextScan() abgelaufen war, stellen wir den Timer neu. Den print-Befehl kann man im Produktionsbetrieb löschen oder auskommentieren
Bleibt noch die obligatorische Tastenabfrage.
Zum Testen wird das Programm whatsapp.py neu gestartet. Jetzt müssen Nachrichten verschickt werden, wenn das Licht länger als warnLevel Sekunden an ist oder/und wenn das Licht ausgemacht wird.
]]>Beim Post zu If-This-Then-That hatten wir Daten vom ESP32 oder ESP8266 an IFTTT geschickt, die uns der Server dann als E-Mail zugesandt hat. Dann hatten wir uns die Messdaten von BME280 und BH1750 durch Thingspeak grafisch darstellen lassen. In dieser Episode wird unser ESP32 Kontakt mit einem Bot auf Telegram aufnehmen. Kein ESP8266? Nein, leider nicht. Warum? Weil das Programm und die Module recht niedlich sind, habe ich es als Erstes mit einem ESP8266 D1 mini probiert. Alles lief perfekt, bis der Kleine einen POST-Request an den Telegram-Bot absetzen sollte. Ich bekam permanent den OSError -40, egal was ich probierte. Im Web wurde ich zu dem Fehler auch nicht fündig. Nach einem vergeblichen Vormittag baute ich die Schaltung für einen ESP32 um. Sieh da - dasselbe Programm, dieselben Module und dieselben Baugruppen - alles schnurrte wie ein Uhrwerk. Irgendetwas im Kernel des ESP8266 muss wohl anders ticken, als beim ESP32, der tickt richtig.
Im Zusammenhang mit dem Telegram-Bot werden wir uns ein wenig mit JSON-Code beschäftigen (JSON = Java Script Objekt Notification). Es gibt starke Ähnlichkeiten mit MicroPython-Objekten. Außerdem bietet das MicroPython-Modul Telegram_ext zwei interessante Features, um eine Klasse zur Laufzeit um anwendungsspezifische Funktionen zu erweitern, ohne in den Programmtext der Klasse eingreifen zu müssen. Klingt interessant? Gut dann legen wir los mit einer neuen Folge aus der Reihe
heute
Mit Telegram erhalten wir die Möglichkeit, nicht nur Nachrichten auf dem PC oder dem Handy zu empfangen, sondern auch noch unseren ESP32 interaktiv zu steuern. Damit schalten wir eine LED, rufen Werte vom DHT22 ab und lassen uns ereignisgesteuerte Sensordaten senden, ohne vorher eine Anfrage zu starten. Wenn Sie also über keinen eigenen internettauglichen Webserver verfügen, dann ist dieser Post der Schlüssel für den weltweiten Zugriff auf Ihre ESP32-Einheit.
Um den Zustand der Schaltung jederzeit auch direkt vor Ort einsehen zu können, habe ich dem ESP ein Display spendiert. Über eine Taste ist ein geordneter Abbruch des Programms möglich, falls zum Beispiel Aktoren sicher ausgeschaltet werden müssen, wie hier die LED.
1 |
ESP32 Dev Kit C unverlötet oder ESP32 Dev Kit C V4 unverlötet oder ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder NodeMCU-ESP-32S-Kit oder |
1 |
|
1 |
KY-021 Magnet Schalter Mini Magnet Reed Modul Sensor oder 10x N/O Reed Schalter Magnetische Schalter 2 * 14mm Magnetischer Induktion Schalter für Arduino + 1 Widerstand 10kΩ |
1 |
|
1 |
|
1 |
LED, zum Beispiel rot |
1 |
Widerstand 330 Ω |
1 |
Widerstand 10 kΩ |
2 |
|
diverse |
Jumper Wire Kabel 3 x 40 STK. je 20 cm M2M/ F2M / F2F evtl. auch |
optional |
Damit neben dem Controller noch Steckplätze für die Kabel frei sind, habe ich zwei Breadboards, mit einer Stromschiene dazwischen, zusammengesteckt.
Abbildung 1: Zwei Breadboards für den ESP32
Eine besondere Erwähnung bedarf der Reed-Kontakt. Er soll am Kontakt S 3,3V liefern, wenn der Kontakt geschlossen ist. Deshalb müssen wir die Anschlüsse an dem Platinchen "+" und "-" so zuordnen, wie in Abbildung 2 rechts. Falls Sie sich für die lose Form des Bauteils entschieden haben, es ist halt unauffälliger anzubringen, dann müssen Sie einen extra 10kΩ-Widerstand mit einbauen, auf dem Modul ist bereits einer vorhanden.
Abbildung 2: So wir der Reed-Kontakt geschaltet
Der Reed-Kontakt wird durch das Annähern eines Magneten zum Durchschalten gebracht. Seine federnden Kontaktstreifen sind ferromagnetisch. In einem Magnetfeld werden die Streifen also selbst magnetisch und ziehen sich gegenseitig an, wodurch der Kontakt geschlossen wird. Entfernt man das Magnetfeld, treiben die Federkräfte die Streifen wieder in ihre Ausgangslage zurück, der Kontakt öffnet.
Abbildung 3: So arbeitet ein REED-Kontakt
Ein wichtiger Tipp!: Seien Sie ganz vorsichtig, wenn Sie die Anschlussdrähte biegen möchten. Sie sind sehr starr. Biegt man sie zu nah am Glaskörper, bricht dieser aus und das Bauteil ist in den ewigen Jagdgründen.
Abbildung 4: Reed-Kontakt
Hier kommt die Schaltung für das Projekt:
Abbildung 5: Schaltung mit Reed-Modul
Thonny oder
ssd1306.py Hardwaretreiber für das OLED-Display
oled.py API für das OLED-Display
telegram_ext.py API für den Telegram-Traffic
telegram.py Betriebssoftware des Projekts
timeout.py Softwaretimer
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 18.06.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Für den PC gibt es eine Software, um Kontakt mit Telegram aufzunehmen. So können Sie sie herunterladen und installieren. Folgen Sie dem Link. Er führt Sie auf diese Seite.
Abbildung 6: Telegram-App für den Desktop-PC herunterladen
Ich habe die Portable Version gewählt, die kann man auch gut auf einem Stick entpacken. Speichern Sie das zip-Archiv in einem beliebigen Verzeichnis und entpacken Sie es dort.
Abbildung 7: Portable-Version entpacken
Im Ordner Telegram finden Sie die Datei Telegram.exe. Erzeugen Sie davon eine Verknüpfung.
Abbildung 8: Von der Anwendung einen Link erzeugen
Die Verknüpfung ziehen Sie auf den Desktop.
Abbildung 9: Verknüpfung zur App auf dem Desktop
Beim Start bringt Windows eine Sicherheitswarnung, klicken Sie auf Ausführen.
Abbildung 10: Nach dem Start
Im Suchfeld links oben im Startfenster geben wir BotFather ein und klicken auf den ersten Listeneintrag.
Abbildung 11: Suche botfather
Abbildung 12: botfather
Mit Klick auf /newbot geht es weiter.
Abbildung 13: Neuen Bot erzeugen
Wir geben in der Eingabezeile den Namen für den neuen Bot ein und schicken die Nachricht mit dem kleinen blauen Dreieck unten rechts an botfather ab.
Abbildung 14: Bot benennen
Der Username für den Bot kann beliebig gewählt werden, muss aber auf bot enden.
Abbildung 15: Benutzername für den Bot festlegen
Nach dem Abschicken der Nachricht bekommen Sie das Token für den API-Zugriff. Kopieren Sie die Zeichenfolge und legen Sie sie an einer sicheren Position ab, wir brauchen sie später.
Abbildung 16: Neuer Bot ist angelegt
Für den Zugriff brauchen Sie ferner die Bot-ID. Sie bekommen diese über den IDBot, den wir zur Fahndung ausschreiben.
Abbildung 17: IDBot suchen
Klicken Sie auf den Listeneintrag und dann auf Starten.
Abbildung 18: IDBot starten
Abbildung 19: User-ID als Antwort
Auch die ID benötigen Sie später.
Sie können nun über den neuen Bot einen ersten Kontakt zum ESP32 aufnehmen, wenn darauf die Anwendung für Ihr Projekt läuft. Damit das läuft, sprechen wir jetzt das Programm dazu durch.
Abbildung 20: Schalten und Werte abrufen
Für das Programm habe ich das Modul utelegram.py benutzt, das ich auf GitHub gefunden habe. Für meine Zwecke habe ich zwei Erweiterungen hinzugefügt und die Methode listen() etwas aufgemufft.
def setCallback (self, f):
self.cb = f
def setExit(self,f):
self.beenden = f
def listen(self):
while True:
self.read_once()
if self.cb is not None:
self.cb()
if self.beenden is not None:
self.beenden()
time.sleep(self.sleep_btw_updates)
gc.collect()
In Python kann man die Referenz auf eine Funktion weitergeben. Diesen Umstand nutzt man bei Closures. Aber auch hier kann man eine tolle Sache damit anstellen.
Üblicherweise läuft die Hauptschleife im Hauptprogramm, wo man sie nach persönlichen Bedürfnissen gestalten kann. Unsere Hauptschleife ist aber die Methode listen() im Modul utelegram.py. Wir könnten unsere Wünsche natürlich umsetzen, indem wir den Code des Moduls verändern. Das müssten wir aber jedes Mal erneut tun, wenn wir das Modul für einen anderen Zweck einsetzen wollen.
Es gibt einen einfacheren Weg, um das Verhalten der Klasse utelegram.ubot zu verändern, ohne den Klassen-Code zu verändern. Die Klasse kann das in zweierlei Formen, die beide denselben Hintergrund haben, Callback-Funktionen. Schauen wir uns zuerst die einfachere, statische Version an.
def setCallback (self, f):
self.cb = f
Ich definiere hier eine Methode setCallback(), die die Referenz auf eine Funktion des Hauptprogramms als Argument nimmt und dem Attribut cb zuweist. Damit versetze ich cb in die Lage als Funktion aufrufbar zu sein und den Code der referenzierten Funktion im Hauptprogramm auszuführen. Weil cb innerhalb ubot deklariert ist, kann ich die Funktion überall in der Klasse verwenden. Das tue ich in der listen-Schleife.
if self.cb is not None:
self.cb()
if self.beenden is not None:
self.beenden()
Beim Programmstart weiß listen() aber noch nicht, was ausgeführt werden soll. Daher belege ich im Konstruktor cb und beenden mit None vor. In listen() passiert nichts, solange diese Belegung aktiv ist.
self.cb = None
self.beenden=None
Im Hauptprogramm übergebe ich schließlich die Namen der im Hauptprogramm deklarierten Funktionen.
bot.setCallback(warnDoorTemp)
bor.setExit(cancelProgram)
Wenn ich andere Funktionen ausführen lassen möchte, oder wenn Änderungen am Code der beiden Funktionen nötig sind, übergebe ich einfach nur die neuen Namen, oder ändere eben den Code. Aber ich muss nicht das Modul ubot neu hochladen, sondern nur das Hauptprogramm neu starten. Und ich kann ubot ohne Veränderung für neue Projekte einsetzen.
Die dynamische Methode, Callback-Funktionen zu nutzen, bedient sich der Dictionaries. Als Schlüssel wird der Name einer Aktion gespeichert und als Wert der Name einer Funktion im Hauptprogramm. Dadurch können wir Befehlscode, den wir per Telegram an den ESP32 gesendet haben, in der Empfangsmethode read_once() parsen und die entsprechende Routine im Hauptprogramm ausführen lassen. Das macht die Methode message_handler(). Brauchen wir einen neuen Befehl, hängen wir den Namen und die auszuführende Funktion einfach an das Dictionary an. Genial, oder?
bot.register('/values', sendValues)
bot.register('/on',switchOn)
bot.register('/off',switchOff)
bot.set_default_handler(get_message)
Durch dieses Vorgehen können wir das Modul utelegram.py jetzt sogar kompilieren, denn wir müssen ja nichts mehr daran ändern. Das spart Platz im Flash und schützt unseren Code.
Nur drei Module in der Importliste sind externe Dateien: timeout.py, telegram_ext.py und oled.py. Letzteres Modul importiert ssd1306.py. Diese vier Dateien müssen also zum ESP32 hochgeladen werden.
from timeout import *
from time import sleep
from machine import Pin, SoftI2C
from oled import OLED
import dht
import network
import Telegram_ext
import sys
Für SSID, Passwort, Token und PID setzen Sie bitte Ihre Daten ein.
mySSID = 'EMPIRE_OF_ANTS'; myPass = "nightingale"
token = 'here goes your token'
PID = "here goes your ID"
Der folgende Block ermittelt automatisch die korrekten Anschlüsse für den I2C-Bus abhängig vom Controllertyp und legt ein I2C-Objekt an.
if sys.platform == "esp8266":
i2c=SoftI2C(scl=Pin(5),sda=Pin(4))
elif sys.platform == "esp32":
i2c=SoftI2C(scl=Pin(22),sda=Pin(21))
else:
raise RuntimeError("Unknown Port")
Das Bus-Objekt übergeben wir an den Konstruktor der OLED-Klasse, stellen die Helligkeit ein und geben die Überschrift aus.
d=OLED(i2c,heightw=64) # 128x64-Pixel-Display
d.contrast(255)
d.writeAt("AMBIANT DATA",2,0)
Dann instanziieren wir das DHT22-Objekt, Eingänge für den Reed-Kontakt, die Taste und einen Ausgang für die LED. Damit nicht ununterbrochen Warnungen zur Temperaturüberschreitung oder offenstehenden Tür eintreffen, definieren wir eine Auszeit von zunächst 10 Sekunden. Den zugehörigen Software-Timer stellen wir auf 1 Sekunde, damit die Überwachung sofort startet.
amb=dht.DHT22(Pin(14)) # D5
reed=Pin(15,Pin.IN) # D3
led=Pin(13,Pin.OUT,value=0) # D7
taste=Pin(17,Pin.IN, Pin. PULL_UP)
deadTime=10 # Sekunden Totzeit zwischen Warnungen
sendNow=TimeOut(1)
Das Dictionary connectStatus übersetzt die Nummerncodes, die wir von nic.status() erhalten, in Klartext.
connectStatus = {
1000: "STAT_IDLE",
1001: "STAT_CONNECTING",
1010: "STAT_GOT_IP",
202: "STAT_WRONG_PASSWORD",
201: "NO AP FOUND",
5: "UNKNOWN",
0: "STAT_IDLE",
1: "STAT_CONNECTING",
5: "STAT_GOT_IP",
2: "STAT_WRONG_PASSWORD",
3: "NO AP FOUND",
4: "STAT_CONNECT_FAIL",
}
Von der Funktion hexMac() erfahren wir die MAC-Adresse des Station-Interfaces. Diese Adresse muss der WLAN-Router kennen, damit er dem ESP32 Zugang gewährt.
def hexMac(byteMac):
"""
Die Funktion hexMAC nimmt die MAC-Adresse im Bytecode
entgegen und bildet daraus einen String fuer die Rueckgabe
"""
macString =""
for i in range(0,len(byteMac)): # Fuer alle Bytewerte
macString += hex(byteMac[i])[2:] # ab Position 2 bis Ende
if i <len(byteMac)-1 : # Trennzeichen
macString +="-"
return macString
Jetzt kommen die Deklarationen der Callback-Routinen. Sie werden von den Methoden listen() und message_handler() aus der Klasse ubot aufgerufen (indirect call).
get_message() wird von ubot.set_default_handler() gerufen, wenn Telegram eine Nachricht erhält, die keiner spezifischen Kennung entspricht und daher keiner speziellen Aktion zugeordnet werden kann. In message wird der empfangene Nachrichtenblock in JSON-Notierung übergeben. Die beiden print-Anweisungen dienen der Darstellung während der Entwicklungszeit und können im Produktionsbetrieb gelöscht oder auskommentiert werden. Die Textnachricht wird aus dem message-Block herausgefiltert und in Großbuchstaben an den PC und ans Handy gesendet.
def get_message(message):
print("Message:\n",message,"\n")
print("Nachricht:\n",message['message']['text'])
bot.send(message['message']['chat']['id'],
message['message']['text'].upper())
Die Syntax der Methode send() ist im Grunde sehr simpel, wirkt aber durch die Struktur des message-Blocks mächtig kompliziert.
bot.send(Telegram-ID, Textnachricht)
Um Licht ins Dunkel zu bringen, schauen wir uns so einen Block näher an. Die JavaScript Object Notation, kurz JSON, erinnert stark an die MicroPython-Objekte Liste, Dictionary und String.
JSON-Element-Typen
Die Elemente von Arrays und Objekten sind durch Kommas getrennt. Dem Komma und dem Doppelpunkt in den Name-Wert-Paaren von Objekten folgt ein Leerzeichen.
Die Methode ubot. read_once() liest vorhandene Nachrichtenblöcke in ein Array ein und übergibt den letzten Eintrag an ubot.message_handler().
messages = self.read_messages()
…
self.message_handler(messages[-1])
Schauen wir uns nun messages und einen message-Block näher an.
Abbildung 21: Ein message-Objekt
Sie meinen, das ist auch nicht weniger verwirrend? Da liegen Sie genau richtig. Ich habe mir mal die Mühe gemacht, das Ganze in eine strukturierte Form zu bringen. Jetzt ist alles ganz einfach. Das Array messages enthält eine message, die mit dem Name-Wert-Paar 'update_id': 'xxxxxxxxxx' beginnt. Der Wert des zweiten Paares mit dem Namen message ist ein Objekt das Objekte, einfache Name-Wert-Paare und ein Array enthält.
Abbildung 22: Hierarchischer Aufbau von messages im JSON-Format
Dann ist message['message']['chat']['id'] '1234567890' und
message['message']['text'] ist 'Nachricht vom ESP32'. Die ID hätte man auch so abrufen können: message['message']['from']['id']. Jetzt stimmen Sie mir sicher zu, dass es einfach ist, Informationen aus dem Block zu bekommen.
Wenn wir an unseren Bot das Kommando /on über die Telegram-App senden, wird die Funktion switchOn() ausgeführt. Wir lassen uns den Wert von message['message']['text'] in Großbuchstaben wandeln, im Terminal ausgeben und, nachdem wir die LED eingeschaltet haben, zurücksenden.
def switchOn(message):
print(message['message']['text'])
code=message['message']['text'].upper()
print(code)
led.on()
bot.send(message['message']['chat']['id'], code)
Analog verhält sich die Funktion switchOff().
def switchOff(message):
print(message['message']['text'])
code=message['message']['text'].upper()
print(code)
led.off()
bot.send(message['message']['chat']['id'], code)
cancelProgram() wird von ubot.listen() aufgerufen, um zu testen, ob die Abbruchtaste gedrückt ist und dann gegebenenfalls das Programm zu beenden. listen() selbst kann die Taste nicht abfragen, weil die Methode vom Vorhandensein des Tastenobjekts nicht den blassesten Schimmer hat. Aber listen() weiß, dass eine Funktion bot.beenden() zyklisch aufgerufen werden muss und bot.beenden kennt die Hausnummer von cancelProgram() und weiß daher, wo angeklopft werden muss. cancelProgram() wiederum kennt das Objekt taste sehr gut und kann den Zustand daher abfragen. Kennen Sie die Story "Der Bauer schickt den Jockel aus…"?
Abbildung 23: Callback Ablauf
Die Funktion warnDoorTemp() ist wieder eine Spur delikater. Warum ist die Funktion sendNow als global ausgewiesen und taste.value in cancelProgram() nicht? Ganz einfach, weil wir eine Fehlermeldung bekämen, wenn wir sendNow nicht als global deklarieren:
Traceback (most recent call last):
File "<stdin>", line 174, in <module>
File "telegram_ext.py", line 69, in listen
File "<stdin>", line 97, in warnDoorTemp
NameError: local variable referenced before assignment
def warnDoorTemp():
global sendNow
amb.measure()
temp=amb.temperature()
message=""
if sendNow():
if reed.value()==0:
message="Door open! "
if temp > 20:
message+="Temperatur zu hoch: {:.2f}°C".\
format(temp)
if message != "":
bot.send(PID,message)
sendNow=TimeOut(deadTime)
taste.value() wird nur referenziert, aber nicht in der Funktion neu deklariert. In Zeile 6 rufen wir sendNow() auf, aber in der letzten Zeile deklarieren wir sendNow() durch den Aufruf von TimeOut() neu. Der MicroPython-Interpreter stuft damit sendNow als lokale Variable ein und erkennt, dass diese referenziert wird, bevor ihr die Referenz auf die Closure compare() zugewiesen wird, die in TimeOut() deklariert wird. Dadurch, dass wir sendNow als globales Objekt ausweisen, machen wir die Abfrage möglich, bevor wir einen neuen Wert zuweisen. Der Zugriff erfolgt jetzt auf die globale Referenz von sendNow. Der Interpreter stößt sich nicht mehr an der nachträglichen Wertzuweisung.
def TimeOut(t):
start=time()
def compare():
nonlocal start
if t==0:
return False
else:
return int(time()-start) >= t
return compare
In warnDoorTemp() holen wir den Temperaturwert und bereiten den Ergebnisstring vor. Falls der Timer abgelaufen ist, prüfen wir den Zustand des Reed-Kontakts und den Wert der Temperatur. Nur wenn einer der beiden Events getriggert wurde, wird eine Nachricht an unseren Telegram-Account gesendet. Danach stellen wir den Wecker neu.
def sendValues(message):
print(message['message']['text'])
code=message['message']['text'].upper()
print(code)
amb.measure()
temp=amb.temperature()
hum=amb.humidity()
d.clearFT(0,1,15,2,False)
d.writeAt("Temp: {:.2f}".format(temp),0,1,False)
d.writeAt("rHum: {:.2f}".format(hum),0,2,False)
code="Temperatur: {:.2f} *C\n".format(temp)
code+="Rel. Feuchte: {:.2f}%\n".format(hum)
bot.send(message['message']['chat']['id'], code)
sendValues() wird gerufen, wenn wir /values an den Bot senden. Messung in Auftrag geben, Temperatur und relative Luftfeuchte abrufen und im Display ausgeben. In code wird die Nachricht zusammengestellt und schließlich versandt.
Nun stellen wir noch die Verbindung zum WLAN-Router her.
nic=network.WLAN(network.AP_IF)
nic.active(False)
nic = network.WLAN(network.STA_IF) # erzeugt WiFi-Objekt nic
nic.active(True) # nic einschalten
sleep(1)
MAC = nic.config('mac') # binaere MAC-Adresse abrufen und
myMac=hexMac(MAC) # in eine Hexziffernfolge umgewandelt
print("STATION MAC: \t"+myMac+"\n") # ausgeben
if not nic.isconnected():
nic.connect(mySSID, myPass)
print("Status: ", nic.isconnected())
d.writeAt("WLAN connecting",0,1)
points="............"
n=1
while nic.status() != network.STAT_GOT_IP:
print(".",end='')
d.writeAt(points[0:n],0,2)
n+=1
sleep(1)
print("\nStatus: ",connectStatus[nic.status()])
d.clearAll()
STAconf = nic.ifconfig()
print("STA-IP:\t\t",STAconf[0],"\nSTA-NETMASK:\t",STAconf[1],\
"\nSTA-GATEWAY:\t",STAconf[2] ,sep='')
d.writeAt(STAconf[0],0,0)
d.writeAt(STAconf[1],0,1)
d.writeAt(STAconf[2],0,2)
sleep(3)
d.clearAll()
d.writeAt("AMBIANT DATA",2,0)
Es fehlt nur noch die Instanziierung des Bots und die Registrierung der Callback-Funktionen, falls eine Verbindung zum Router zustande gekommen ist. register() baut das Dictionary ubot.commands auf. set_default_handler(),
setCallback() und setExit() machen die entsprechenden Funktionen in ubot bekannt. Final wird mit bot.listen() die Empfangsschleife betreten.
if nic.isconnected():
bot = telegram_ext.ubot(token)
bot.register('/values', sendValues)
bot.register('/on',switchOn)
bot.register('/off',switchOff)
bot.set_default_handler(get_message)
bot.setCallback(warnDoorTemp)
bot.setExit(cancelProgram)
print('BOT LISTENING')
bot.listen()
else:
print('NOT CONNECTED - aborting')
Das Programm wird beendet, falls keine Verbindung zum Router zustande gekommen ist.
Hier noch einmal alle Schritte, um das Projekt zum Laufen zu bringen.
Bis jetzt sind aller guten Dinge drei, in der nächsten Folge zum Thema Messaging behandeln wir abschließend den Dienst Whatsapp.
Bis dann, bleiben Sie dran!
]]>Nach IFTTT wollen wir uns heute einen weiteren Dienst im Web anschauen. Er wird uns helfen, Messdaten von unserem ESP32 ganz einfach als Grafik-Plot darzustellen. Wie IFTTT ist dieser Service kostenlos und enthält nicht einmal Beschränkungen bezüglich der Anzahl von Applets. War es bei IFTTT beim HTTP-Zugriff die Methode POST, so wird es hier die Methode GET sein. Was ist der Unterschied? Bei GET erfolgt der Request an den Server in einem Zugriff. Sie kennen das vielleicht von der Darstellung von Suchanfragen, etwa der folgenden Form.
https://server.domain.com/dateien/templates?app=openoffice&revision=23
Aus solchen Zeilen baut der Browser einen GET-Request zusammen. Diese Art Anfragen sind auf relativ wenige Zeichen begrenzt. Wie viele maximal in Frage kommen, entscheidet der jeweilige Server.
Ein POST-Request geschieht in zwei Schritten. Zuerst wird eine Verbindung aufgebaut und im zweiten Schritt werden die Daten verschifft. Diese Art von Anfrage wird verwendet, wenn viele Daten gesendet werden sollen, etwa aus einem HTML-Formular.
Manche Server akzeptieren beide Methoden, andere schreiben die Methode vor. Sie können das ja gerne ausprobieren. Wie ein POST zusammengesetzt wird, zeigt der Beitrag zu IFTTT.
Aber jetzt werfen wir erst einmal einen Blick auf ThingSpeak. So heißt der Dienst, den ich ihnen in dieser Episode aus der Reihe
heute
Sie vermissen hier den ESP8266? Ja, den habe ich weggelassen, weil sein Speicher für den Treiber des BME280 gehörig zu schmalbrüstig ist. Grundsätzlich können natürlich auch die Werte von einem ESP8266 an ThingSpeak gesendet werden. Nur müssen Sie dann auf andere Sensoren ausweichen, die einen schlankeren Treiber benutzen, wie zum Beispiel der SHT21 oder der AHT10. Bei diesen Modulen fehlt aber der Luftdrucksensor.
Außerdem ist heute ein weiterer Sensor mit an Bord, ein BH1750. Er wird, wie der BME280, über den I2C-Bus angesteuert und liefert Helligkeitswerte in Lux. Chef vom Dienst ist bei mir ein ESP32 Dev Kit V4. Natürlich eignen sich auch die anderen Familienmitglieder, die in der folgenden Liste aufgeführt sind.
Um den Zustand der Schaltung jederzeit auch direkt vor Ort einsehen zu können, habe ich dem ESP ein Display spendiert. Über die Flash-Taste ist ein geordneter Abbruch des Programms möglich, falls zum Beispiel Aktoren sicher ausgeschaltet werden müssen, wie hier die LED.
1 |
ESP32 Dev Kit C unverlötet oder ESP32 Dev Kit C V4 unverlötet oder ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder NodeMCU-ESP-32S-Kit oder |
1 |
|
1 |
GY-BME280 Barometrischer Sensor für Temperatur, Luftfeuchtigkeit und Luftdruck |
1 |
LED, zum Beispiel rot |
1 |
Widerstand 330 Ω |
2 |
|
1 |
|
diverse |
Jumper Wire Kabel 3 x 40 STK. je 20 cm M2M/ F2M / F2F evtl. auch |
optional |
Damit neben dem Controller noch Steckplätze für die Kabel frei sind, habe ich zwei Breadboards, mit einer Stromschiene dazwischen, zusammengesteckt.
Abbildung 1: Aufbau Ambiant-Meter
Und hier ist der Schaltplan:
Abbildung 2: Schaltung Ambiant-Meter
Thonny oder
ssd1306.py Hardwaretreiber für das OLED-Display
oled.py API für das OLED-Display
bme280.py Treiber für das BME280-Modul
bh1750.py Treiber für den Lichtsensor BH1750
urequests.py Treibermodul für den HTTP-Betrieb
timeout.py Softwaretimer-Modul
thingSpeak.py Demoprogramm für ThingSpeak
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 18.06.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Folgen Sie mir auf die Startseite von ThingSpeak.
Abbildung 3: Startfenster (Quelle: https://thingspeak.com)
Beim ersten Aufruf erscheint oben die Sprachauswahl. Weiter geht es mit Get startet for free.
Abbildung 4: Account einrichten (Quelle: https://thingspeak.com)
Als erstes muss man einen MathWorks-Account einrichten. Geben Sie hier Ihre E-Mail-Adresse ein. Mit Next geht es weiter zur Erfassung einiger persönlicher Daten.
Abbildung 5: Persönliche Daten erfassen (Quelle: https://thingspeak.com)
Continue, führt zum nächsten Fenster. Setzen Sie den Haken bei Use this email for my MathWorks Account – Continue.
Abbildung 6: Private Adresse für den Account verwenden (Quelle: https://thingspeak.com)
Abbildung 7: Mailadresse verifizieren (Quelle: https://thingspeak.com)
Jetzt sollten Sie in Ihre Mailbox schauen, ob Sie eine Mail bekommen haben, mit der Sie per Link die Mail-Adresse bestätigen.
Abbildung 8: E-Mailadresse bestätigen (Quelle: https://thingspeak.com)
Nun wird noch ein Passwort für den neuen Account erwartet. Es muss nicht das von Ihrem E-Mail-Provider sein. Achtung, das Passwort wird nicht verifiziert, das Häkchen setzen und - Continue.
Abbildung 9: Passwort erzeugen (Quelle: https://thingspeak.com)
Im nachfolgenden Fenster wählen Sie einen der Anwendungsfälle aus. Dann landen Sie auf der Seite, auf der Sie Ihre Kanäle verwalten können. Weil noch keiner existiert, klicken Sie auf Neuer Kanal. Falls Sie von der Startseite von ThingSpeak kommen, öffnen Sie im Menü Kanäle den Punkt Meine Kanäle. Damit landen Sie auch auf der Verwaltungsseite.
Abbildung 10: Startfenster nach Erstellung des Accounts (Quelle: https://thingspeak.com)
Klicken Sie auf Neuer Kanal:
Abbildung 11: Neuen Kanal erzeugen (Quelle: https://thingspeak.com)
Für unser Projekt brauchen wir vier Felder für Temperatur, rel. Luftfeuchte, Luftdruck und Helligkeit. Als Name habe ich Tageswerte gewählt. Bevor die Felder beschriftet werden können, muss das Häkchen gesetzt werden. Dann: Save Channel.
Abbildung 12: Meine Felder (Quelle: https://thingspeak.com)
Als Zusammenfassung bekommen wir eine Übersicht angezeigt:
Abbildung 13: Zusammenfassung (Quelle: https://thingspeak.com)
Im Ordner API Keys bekommen Sie alle erforderlichen Informationen, die zum Senden von Requests nötig sind. Da sind erst einmal die Schlüssel für den Schreib- und Lesezugriff:
Abbildung 14: Die Zugriffsschlüssel für schreiben und lesen (Quelle: https://thingspeak.com)
Außerdem finden Sie rechts unten die URL-Zeilen für den Schreib- und Lese-Zugriff. Die Zeilen können ohne das GET in das Adressfeld eines Browsers kopiert werden, um den gewünschten Vorgang auszulösen. Wir werden die Zeile für das Schreiben in erweiterter Form für unsere vier Messwerte ins Programm kopieren.
Abbildung 15: Zugriff auf den Kanal mit GET (Quelle: https://thingspeak.com)
Wie üblich beginnen wir mit dem Importgeschäft. Folgende Dateien müssen zuvor in den Flash des ESP32 hochgeladen werden: ssd1306.py, oled.py, bh1750.py, bme280.py und timeout.py. Alles andere wird durch den Kernel von MicroPython zur Verfügung gestellt.
from machine import Pin, SoftI2C
from time import sleep
from oled import OLED
from bh1750 import BH1750
from bme280 import BME280
import requests
import network, socket
import sys, os
from timeout import *
import gc
Eine Besonderheit stellt der Import von timeout dar. Durch den Stern werden sämtliche Objekte direkt in den globalen Scope (ak Namensraum) des Hauptprogramms ThingSpeak.py übernommen. Damit entfällt das, sonst übliche, Objekt-Prefix beim Aufruf der Funktionen.
Es folgt die Bekanntgabe der API-Schlüssel und der Kanalnummer.
thspkWriteKey="---Ihr Schreibschlüssel---"
thspkReadKey="---Ihr Leseschlüssel---"
thspkCannel="---Ihre Kanalnummer---"
intervall=1000 # ms bis zur ersten Erfassung
folgeIntervall=60000
mySSID = 'EMPIRE_OF_ANTS'; myPass = "nightingale"
Das Intervall vom Start der Hauptschleife bis zur ersten Erfassung setze ich hier auf eine Sekunde, die Folgeintervalle auf eine Minute. Es folgen die Credentials für die Kontaktaufnahme mit dem WLAN-Router.
Der Programmbaustein für das Instanziieren des I2C-Busses wäre hier nicht erforderlich, weil eh nur ein ESP32 das Programm stemmen kann. Ich spare mir halt nach dem Einfügen durch copy and paste das Trimmen.
if sys.platform == "esp8266":
i2c=SoftI2C(scl=Pin(5),sda=Pin(4))
elif sys.platform == "esp32":
i2c=SoftI2C(scl=Pin(22),sda=Pin(21))
else:
raise RuntimeError("Unknown Port")
Mit dem I2C-Objekt erzeugen wir gleich das Display-Objekt, die BME280- und die BH1750-Instanz.
d=OLED(i2c,heightw=64) # 128x64-Pixel-Display
d.contrast(255)
d.writeAt("AMBIANT DATA",2,0)
bme=BME280(i2c)
bh=BH1750(i2c)
Das Dictionary (kurz Dict) connectStatus übersetzt die nichtssagenden Nummern, die die Funktion nic.status() beim Verbindungsaufbau mit dem Router zurückgibt, in Klartext.
connectStatus = {
1000: "STAT_IDLE",
1001: "STAT_CONNECTING",
1010: "STAT_GOT_IP",
202: "STAT_WRONG_PASSWORD",
201: "NO AP FOUND",
5: "UNKNOWN",
0: "STAT_IDLE",
1: "STAT_CONNECTING",
5: "STAT_GOT_IP",
2: "STAT_WRONG_PASSWORD",
3: "NO AP FOUND",
4: "STAT_CONNECT_FAIL",
}
Mit der Funktion hexMac() erfahren wir die MAC-Adresse des Station-Interfaces des Controllers im Klartext. Diese muss im Router eingetragen werden, sonst verweigert dieser dem Controller den Zugang. Meistens geschieht der Eintrag über das Menü WLAN-Sicherheit. Das genaue Vorgehen verrät das Handbuch des Routers.
Wir tasten das Bytes-Objekt, das uns der Funktionsaufruf nic.config('mac') liefert, Zeichen für Zeichen ab, machen daraus eine Hexadezimalzahl und bauen daraus den String auf, den die Funktion zurückgibt.
def hexMac(byteMac):
"""
Die Funktion hexMAC nimmt die MAC-Adresse im Bytecode
entgegen und bildet daraus einen String fuer die Rueckgabe
"""
macString =""
for i in range(0,len(byteMac)): # Fuer alle Bytewerte
macString += hex(byteMac[i])[2:] # ab Position 2 bis Ende
if i <len(byteMac)-1 : # Trennzeichen
macString +="-"
return macString
Händisch sieh das so aus:
>>> nic.config('mac')
b'\xf0\x08\xd1\xd1m\x14'
>>> hex(m[0])
'0xf0'
>>> hex(m[0]) [2:]
'0xf0'
>>> hex(m[0])[2:]
'f0'
Es folgt die Verbindungsaufnahme mit dem WLAN-Router. Dazu schalten wir das Station-Interface ein, der Controller arbeitet ja als Client. Die Schaltsekunde danach ist wichtig und vermeidet interne WLAN-Fehler, die sporadisch auftreten.
# ********************* Bootsequenz ************************
#
nic = network.WLAN(network.STA_IF) # erzeugt WiFi-Objekt nic
nic.active(True) # nic einschalten
sleep(1)
MAC = nic.config('mac') # binaere MAC-Adresse abrufen und
myMac=hexMac(MAC) # in eine Hexziffernfolge umgewandelt
print("STATION MAC: \t"+myMac+"\n") # ausgeben
Wir lesen die MAC-Adresse aus, lassen sie in Klartext umformen und im Terminal ausgeben.
if not nic.isconnected():
nic.connect(mySSID, myPass)
print("Status: ", nic.isconnected())
d.writeAt("WLAN connecting",0,1)
points="............"
n=1
while nic.status() != network.STAT_GOT_IP:
print(".",end='')
d.writeAt(points[0:n],0,2)
n+=1
sleep(1)
Wenn die Schnittstelle noch keine Verbindung zum Router hat, stellen wir mit connect() eine her. Dabei werden SSID und Passwort übertragen. Der Status wird abgefragt und ausgegeben. Solange wir vom DHCP-Server des Routers noch keine IP-Adresse bekommen haben, wird im Sekundenabstand ein Punkt ausgegeben. Das sollte nicht länger als 4 bis 5 Sekunden dauern.
Dann fragen wir den Status ab und lassen uns die Verbindungsdaten, IP-Adresse, Netzwerkmaske und Gateway, mitteilen.
print("\nStatus: ",connectStatus[nic.status()])
d.clearAll()
STAconf = nic.ifconfig()
print("STA-IP:\t\t",STAconf[0],"\nSTA-NETMASK:\t",STAconf[1],\
"\nSTA-GATEWAY:\t",STAconf[2] ,sep='')
d.writeAt(STAconf[0],0,0)
d.writeAt(STAconf[1],0,1)
d.writeAt(STAconf[2],0,2)
sleep(3)
d.clearAll()
d.writeAt("AMBIANT DATA",2,0)
nextMeasurement=TimeOutMs(intervall)
Die erste Messwertaufnahme wird vorbereitet, indem wir den Timer nextMeasurement auf 1000 ms stellen. Dann geht es in die Mainloop, die nur zwei Events bedient, den abgelaufenen Timer und die Tastenabfrage.
Dazu noch eine kurze Bemerkung. TimeOut() ist eine Funktion, in deren Codekörper eine weitere Funktion mit dem Namen compare() deklariert wird. compare() ist eine sogenannte Closure. Das Besondere daran ist, dass diese Funktion nach dem Verlassen der umschließenden Funktion TimeOut() nicht kompostiert wird, sondern weiterhin referenziert werden kann. Das wird durch zwei Umstände möglich gemacht. Der eine ist, dass compare() die an TimeOut() übergebene Zeit referenziert und ferner den Zeitstempel in start. Der zweite Grund ist, dass TimeOut() eine Referenz auf compare() zurückgibt. Wir speichern sie hier in nextMeasurement ab. nextMeasurement ist damit callable, wir referenzieren beim Aufruf letztlich die Closure compare(). Wenn Sie mehr über Closures erfahren wollen, folgen Sie diesem Link.
def TimeOutMs(t):
start=ticks_ms()
def compare():
nonlocal start
if t==0:
return False
else:
return int(ticks_ms()-start) >= t
print (id(compare))
return compare
>>> nextMeasurement=TimeOutMs(intervall)
1073713376
>>> id(nextMeasurement)
1073713376
Hier haben Sie den Beweis, die IDs von compare() und nextMeasurement() sind identisch
In der Hauptschleife fragen wir zuerst den Timer ab, indem wir die Funktion compare() via nextMeasurement() aufrufen. Solange der Timer noch läuft, erhalten wir als Rückgabe False. Ist aber ticks_ms – start gößer als 1000, bekommen wir True und betreten den Codekörper des if-Konstrukts.
while 1:
if nextMeasurement():
red.on()
nextMeasurement=TimeOutMs(folgeIntervall)
temp=bme.calcTemperature()
hum=bme.calcHumidity()
pres=bme.calcPressureNN()
lum=bh.luminanz()
Die LED wird angeschaltet und der Timer neu gestartet, dieses Mal mit dem 60-Sekunden-Intervall. Danach holen wir die Werte von den Sensoren und geben sie auf dem Display aus. Das False in den ersten vier Zeilen bewirkt, dass der Text nur in den Datenpuffer im ESP32 geschrieben wird. In der 5. Zeile kommt der optionale Keyword-Parameter show mit dem Defaultwert True zum Tragen. Erst jetzt wird der Pufferinhalt zum Display geschickt. Dieses Vorgehen beruhigt das Flackern der Anzeige.
d.clearFT(0,1,15,4,False)
d.writeAt("Temp: {:.2f}".format(temp),0,1,False)
d.writeAt("rHum: {:.2f}".format(hum),0,2,False)
d.writeAt("Pres: {:.2f}".format(pres),0,3,False)
d.writeAt("Lumi: {:.2f}".format(lum),0,4)
Dann senden wir einen GET-Request an ThingSpeak. Alles, was wir dazu tun müssen ist, einen URL-String nach dem oben genannten Muster zu erzeugen. Die Zahlenwerte basteln wir durch die Formatierungsstrings {:.2f} in den Text. Wir senden in einem Abwasch alle vier Werte.
r=requests.get("https://api.ThingSpeak.com/update?api_key="+ thspkWriteKey+"&field1={:.2f}".format(temp)+"&field2={:.2f}".format(hum)+"&field3={:.2f}".format(pres)+"&field4={:.2f}".format(lum))
print(r.reason.decode(),r.text)
status=r.reason.decode()+"; "+r.text
d.writeAt(status,0,5)
r.close()
Die Antwort des Servers legen wir in der Variablen r ab. r ist eine Instanz der Klasse Response, die im Modul requests definiert ist. Den Übertragungsstatus in reason und den Text der Antwort geben wir im Terminal und im Display aus. Danach schließen wir die Verbindung ordnungsgemäß. Täten wir das nicht, bekämen wir beim nächsten Durchlauf eine Fehlermeldung mit Programmabbruch.
Der Rest ist schnell erledigt. Falls die Taste gedrückt ist, wird die LED gezielt ausgeschaltet und das Programm verlassen.
if taste.value()==0:
red.off()
sys.exit()
Auf der Seite https://ThingSpeak.com/channels/XXXXXXX/private_show können Sie jetzt die Aufzeichnung der Messwerte verfolgen. Natürlich müssen Sie die X-e durch Ihre Kanalnummer ersetzen.
In der nächsten Folge geht es um einen Telegram-Bot. Damit werden wir in der Lage sein, nicht nur Daten zu empfangen, sondern auch Schaltbefehle zu erteilen.
Bis dann!
]]>In diesem Projekt werden wir eine kleine hölzerne Box konstruieren, um Geister und alle, die sich mit ihrem „Süßes oder Saures“ nähern, abzuschrecken. Sie erwartet eine gruselige Überraschung in der Kiste.
Es wird uns ein Monster durch die Löcher der hölzernen Kiste beobachten. Man wird seine Anwesenheit spüren, da es versuchen wird, aus der Kiste zu entkommen und die Kiste dadurch bewegt wird. Wenn man sich nähert, wird es denjenigen mit seinen roten Augen durch eines der Löcher beobachten. Wenn man sich weiterhin nähert, wird es den Deckel der Kiste anheben, um besser sehen zu können. Nähert man sich dann noch weiter, um die „Süßes oder Saures“-Belohnung zu nehmen, wird es seine Klaue durch ein Loch stecken, um denjenigen zu fangen.
Beginnen wir mit dem Projekt für Halloween 2023.
Wir verwenden den Ultraschallsensor HC-SR04, um die Entfernung zu Personen zu messen. Für die Soundwiedergabe nutzen wir ein Set bestehend aus dem DFPlayer Modul und dem Lautsprecher DFPlayer Mini. Für die erforderlichen Bewegungen verwenden wir fünf MG90S-Servomotoren, die wir mit dem Modul PCA9685 16-Kanal 12-Bit-PWM-Servotreiber steuern. Um die Augen des Monsters zu simulieren, verwenden wir vier rote LEDs, an die zusätzlich jeweils einer von vier 330-Ohm-Widerstände angeschlossen werden muss. Außerdem verwenden wir einen 1K Ohm-Widerstand im MP3-Player-Modul, um die Spannung auf der Kommunikationsleitung anzupassen.
Alle zuvor genannten Module werden vom AZ-ATmega328-Board-Mikrocontroller gesteuert. Das gesamte System wird mit 5V Gleichstrom über ein Netzteil betrieben. Alternativ kann auch ein Batteriepack mit 5V Gleichstrom verwendet werden.
Funktionstest der Komponenten ohne Box:
Wenn die Videos nicht angezeigt werden, überprüfen Sie bitte die Cookie-Einstellungen Ihres Browsers
Ich habe für den Anfang Bauteile aus Pappe zugeschnitten. Später habe ich das auf Holz übertragen.
Das erste, was zu montieren ist, ist der Boden der Box. Dann müssen wir die Stützen installieren, um die Servomotoren 0 und 4, die die Box bewegen zu montieren. In der Mitte des Bodens muss ein Loch gebohrt werden, um alle Kabel zu verlegen, die die Komponenten im Inneren der Box mit dem Mikrocontroller, dem MP3-Player, den Widerständen und dem PCA3685-Modul zu verbinden.
Dann installieren wir die linke Wand, an der der Servomotor 1 montiert wird. Außerdem die Baugruppe zum Öffnen der Box und Servomotor 2, der die Augen beim Öffnen der Box bewegt.
Dann folgt die rechte Wand für den Servomotor 3, der den Arm des Monsters bewegt.
Wir müssen nun das Teil montieren, das den Deckel öffnet. Es wird mit dem Servomotor verbunden, der die Augen des Monsters bewegt. Dieses Teil muss zuerst am Servomotor 1 befestigt werden, bevor der Servomotor 2 mit dem Bauteil mit den zwei beweglichen LEDs installiert wird.
Die Baugruppe, die die Klaue bewegt, besteht aus zwei Teilen. Sie wird später am Servomotor 3 befestigt.
Außerdem müssen wir eine Halterung für den Lautsprecher und den Ultraschallsensor anbringen.
Die Wände sollten kleine 3 mm Aussparungen haben, um eine Seite mit der anderen verbinden zu können.
In die Außernseiten der Box schneiden wir Rillen, um Holzbretter zu imitieren.
Zuerst werden die rechteckigen Teile an der Außenseite der Box zusammengeklebt. Zwei kürzere oben, zwei längere an den Seiten.
Auf der Innenseite der Seitenflächen der Box werden die Platten für den Servomotor installiert, um sie zu bewegen.
Der Deckel der Box hat größere Abmessungen, da er den gesamten äußeren Umfang der Seitenflächen abdecken muss. An der Rückseite und der Oberseite werden zwei kleine Platten als Scharniere angebracht.
Sobald wir die Box zusammengebaut haben, beginnen wir mit der Installation der Komponenten.
Zuerst installieren wir die Servomotoren 0 und 4, die die Box bewegen werden. Dann installieren wir den Lautsprecher und anschließend den Ultraschallsensor HC-SR04. Die Kabel werden durch das Loch geführt.
Jetzt werden wir den Servomotor 1 in seiner Halterung befestigen, danach das Teil, das den Kasten öffnet. Anschließend der Servomotor 2 mit dem Satz beweglicher LEDs.
Jetzt ist der Servomotor 3 an der Reihe, der den Arm des Monsters bewegt. Wenn wir ihn befestigt haben, bauen wir den Arm des Monsters ein. Wir müssen die kleine Halterung montieren, in der die Führungswelle des Arms installiert wird.
Wir dürfen nicht vergessen, die beiden festen roten LEDs auf der Halterung des Servomotors 1 und die beiden beweglichen LEDs auf dem Servomotor 2 zu installieren.
Um die Löcher zu schneiden, hinter denen sich die LEDs und der Sensor befinden, habe ich eine transparente Folie als Schablone verwendet. Die Öffnungen habe ich dann auf das Bauteil übertragen und ausgeschnitten.
Die Bauteile sind aus Holz gefertigt. Die inneren Teile und das Innere der Kistenwände werden schwarz bemalt, damit sie von außen nicht sichtbar sind.
Die beiden Servomotoren, die die Kiste bewegen, sind in das Bodenbauteil eingebaut. An der linken Wand sind die beiden statischen LEDs und der Servomotor installiert, der den Kistendeckel anhebt. Außerdem der Servomotor, der zwei LEDs bewegt. An der rechten Wand ist der Servomotor installiert, der über einen Mechanismus die Kralle aus der Kiste bewegt.
Ich habe hier eine Kralle verwendet, aber beispielsweise kann auch die Hand eines Skelets angebracht werden. An den beiden Seiten der Kiste müssen wir zwei kleine Regale installieren, damit die seitlichen Servomotoren den Kasten anheben.
Funktionstest der Komponenten in der Box:
Der Ultraschallsensor HC-SR04 sendet Signale aus und wenn ein Signal von einem Objekt reflektiert wird, erfasst der Sensor dieses empfangene Signal. Der Mikrocontroller berechnet die Zeit, die das Signal vom Absenden bis zum Empfang benötigt hat. Mit Hilfe der Schallgeschwindigkeit in der Luft wird dann die Entfernung berechnet. Solange der Abstand 200 cm oder mehr beträgt, passiert nichts. Wenn ein Objekt oder eine Person zwischen 200 und 150 cm erkannt wird, leuchten die beiden roten LEDs „leds_static_eyes“ in der Box auf und die Datei 4.mp3 wird wiedergegeben, die auf einer in den MP3-Player-Modul eingesteckten MicroSD-Karte gespeichert werden muss. Wenn wir uns weiter annähern und uns auf eine Entfernung zwischen 150 und 100 cm befinden, bewegt sich die Kiste seitlich mit den Servomotoren 0 und 4. Außerdem wird die Datei 2.mp3 abgespielt. Wenn wir uns noch weiter annähern und uns auf eine Entfernung zwischen 100 und 50 cm positionieren, ändert der Servomotor 1 seine Position, wodurch der Deckel der Kiste langsam geöffnet wird, die beiden anderen roten LEDs "servo_leds_eyes" leuchten auf und bewegen sich leicht hin und her, gesteuert durch Servomotor 2. Zusätzlich wird die Datei 1.mp3 abgespielt. Schließlich, wenn wir uns auf eine Entfernung von 50 cm oder weniger nähern, leuchten wieder die beiden LEDs "leds_static_eyes" auf und der Servomotor 3 ändert seine Position, sodass eine kleine Klaue aus der Kiste herausragt. Sie bewegt sich leicht, als ob sie uns greifen möchte. Dazu wird die Datei 2.mp3 abgespielt.
Alle zuvor beschriebenen Aktionen werden fortlaufend wiederholt, solange wir uns in der entsprechenden Entfernung befinden.
Ich erkläre nun den Sketch (Programmcode).
Der erste Schritt in jedem Sketch besteht darin, die erforderlichen Bibliotheken zu inkludieren, damit wir die verwendeten Module nutzen können. Die ersten beiden hinzugefügten Bibliotheken sind erforderlich, um das Mini MP3 DFPlayer-Modul zu verwenden. Für die Kommunikation zwischen dem MP3-Modul und dem Mikrocontroller aktivieren wir mit der SoftwareSerial.h-Bibliothek die Funktionalität, mit der der Mikrocontroller einen beliebigen digitalen Pin als serielle Schnittstelle verwenden kann. Mit der zweiten Bibliothek (DFRobotDFPlayerMini.h) aktivieren wir die erforderlichen Funktionen zur Verwendung des Moduls.
#include <SoftwareSerial.h> #include <DFRobotDFPlayerMini.h>
Die nächsten beiden Bibliotheken werden hinzugefügt, um das PCA9685-Servomotor-Steuermodul zu verwenden. Die erste davon, Wire.h, wird für die I2C-Kommunikation zwischen dem Mikrocontroller und dem PCA9685-Modul benötigt. Mit der Adafruit_PWMServoDriver.h-Bibliothek aktivieren wir die erforderlichen Funktionen, um die Servomotoren zu steuern.
#include <Wire.h> #include <Adafruit_PWMServoDriver.h>
Die letzte hinzugefügte Bibliothek ist "SR04.h". Diese Bibliothek aktiviert die Methoden und Anweisungen, die wir verwenden werden, um mit dem Ultraschallsensor-Modul zu arbeiten.
#include "SR04.h"
(abhängig davon, wie Sie die Bibliotheken installieren, werden sie entweder mit doppelten Anführungszeichen, oder in spitzen Klammern inkludiert)
Nachdem wir die erforderlichen Bibliotheken hinzugefügt haben, müssen wir für jedes Modul oder jeden erforderlichen Bestandteil ein Objekt implementieren. Um das MP3-Player-Modul zu verwenden, müssen wir das Objekt "mySoftwareSerial" aus der Bibliothek "SoftwareSerial.h" implementieren, um dem Mikrocontroller die verwendeten digitalen Pins für die serielle Kommunikation mit dem Modul mitzuteilen. In diesem Projekt verwenden wir den digitalen Pin 10 zum Empfangen von Daten und den digitalen Pin 11 zum Senden von Daten an das MP3-Modul. Um die Methoden und Anweisungen zur Steuerung des Moduls zu verwenden, wie z.B. die Lautstärkeanpassung oder das Starten der Wiedergabe einer MP3-Datei, erstellen wir das Objekt "myDFPlayer" aus der Bibliothek "DFRobotDFPlayerMini.h".
SoftwareSerial mySoftwareSerial(10, 11); DFRobotDFPlayerMini myDFPlayer;
Für das PCA9685-Modul wird das Objekt "servoDriver_module" aus der Bibliothek "Adafruit_PWMServoDriver.h" erzeugt. Außerdem der minimale und maximale Pulsweitenwert für die Servomotoren. Das entspricht den Positionen 0 Grad und 180 Grad.
Adafruit_PWMServoDriver servoDriver_module = Adafruit_PWMServoDriver(); #define SERVOMIN 100 #define SERVOMAX 500
Die Pins für das Ultraschallsensor-Modul werden als Konstanten deklariert. Wir müssen auch ein SR04-Objekt implementieren und eine Variable für das Ergebnis der berechneten Distanz erstellen. Der TRIG-Pin wird mit Pin 3 des Mikrocontrollers verbunden. Darüber wird das Aussenden des Ultraschallsignals angestoßen (getriggert). Der ECHO-Pin ist Pin 2 am Mikrocontroller. Das ist das empfangene Signal (Echo). Daraus wird die Entfernung berechnet.
#define TRIG_PIN 3 #define ECHO_PIN 2 SR04 ultrasonics_sensor = SR04(ECHO_PIN,TRIG_PIN); long distance;
Die letzten beiden Zeilen dieses Abschnitts sind die Variablen für die Ausgangspins des Mikrocontrollers, an die die LEDs angeschlossen sind. Der Variablen "leds_static_eyes" weisen wir den Wert 4 zu. Die Variable "servo_leds_eyes" bekommt den Wert 5, also Pin 4 und Pin 5 für jeweils zwei rote LEDs.
int leds_static_eyes = 4; int servo_leds_eyes = 5;
Danach folgt nun der Abschnitt für die setup()-Methode
In den ersten beiden Zeilen initialisieren wir die Kommunikation an der Software-Serial-Schnittstelle für das MP3-Modul mit der Anweisung "mySoftwareSerial.begin(9600)". Für die serielle Konsole verwenden wir "Serial.begin(115200)". Beachten Sie die unterschiedlichen Baudraten, das ist Absicht.
mySoftwareSerial.begin(9600); Serial.begin(115200);
Als Nächstes überprüfen wir, ob das MP3-Modul initialisiert wurde. Hierfür verwenden wir eine bedingte Anweisung. Wir verwenden das Ausrufezeichen (!) als Negierung, um abzufragen, ob das MP3-Modul nicht initialisiert wurde. Wenn das der Fall ist, wird der Code innerhalb der geschweiften Klammern ausgeführt. In diesem Fall geben wir über die serielle Konsole eine Meldung aus, in der wir die Überprüfung der Verbindungen und des Einsetzens der microSD-Karte anweisen. Wenn das MP3-Modul erfolgreich initialisiert wurde, wird die Bedingung nicht erfüllt und der Code wird fortgesetzt. Dann wird über die serielle Konsole eine Meldung auszugeben, dass das DFPlayer-Modul ordnungsgemäß initialisiert wurde.
if (!myDFPlayer.begin(mySoftwareSerial)) { Serial.println(F("Error initializing mp3 module:")); Serial.println(F("1. Please check the connections!")); Serial.println(F("2. Please insert the microSD memory!")); while(true){ delay(0); } } Serial.println(F("Correct DFPlayer initialization."));
Danach folgt die Initialisierung das PCA9685-Moduls mit der Methode begin(). In nächsten Zeile legen wir die Arbeitsfrequenz der Servomotoren mit der Funktion setPWMFreq(50) auf 50 Hz fest. Anschließend rufen wir die Methode home() auf, um die Servomotoren in die Ausgangspositionen zu bringen.
servoDriver_module.begin(); servoDriver_module.setPWMFreq(50); home();
In den nächsten vier Zeilen konfigurieren wir die digitalen Pins, an die wir die LEDs angeschlossen haben, als Ausgang und setzen den anfänglichen Zustand beider Pins auf LOW, wodurch die LEDs nicht leuchten.
pinMode (leds_static_eyes, OUTPUT); digitalWrite (leds_static_eyes, LOW); pinMode (servo_leds_eyes, OUTPUT); digitalWrite (servo_leds_eyes, LOW);
In den beiden letzten Zeilen der setup()-Methode konfigurieren wir Trigger- und Echo-Pin des Ultraschallmoduls am Mikrocontroller als Aus- bzw. Eingang.
pinMode (TRIG_PIN, OUTPUT); pinMode (ECHO_PIN, INPUT);
Wir haben jetzt unsere Projektmodule vollständig konfiguriert. Jetzt müssen wir die loop()-Methode programmieren, damit das gesamte Set die gewünschten Bewegungen ausführt. Die erste Zeile in der Methode ist ein Aufruf der Methode measure_distance(). Diese Methode misst die Entfernung zwischen einem Objekt und dem Ultraschallsensor. In der Zeile distance = ultrasonics_sensor.Distance() messen wir den Abstand zum nächsten Objekt und speichern seinen Wert in der zuvor deklarierten Variablen distance. Mit den nachfolgenden drei Zeilen geben wir den gemessenen Entfernungswert über die serielle Konsole aus. Die letzte Zeile der Methode erzeugt eine Pause von 100 Millisekunden.
measure_distance(); . . . . . . . . . . . . . . . void measure_distance() { distance = ultrasonics_sensor.Distance(); Serial.print("Distance to obstacles "); Serial.print(distance); Serial.println(" cm"); delay(100); }
Die Ausführung des folgenden Codes ist abhängig von der gemessenen Entfernung. Ist der Abstand kleiner als 200 cm und größer oder gleich 150 cm, wird zuerst die Lautstärke verändert. Anschließend wird die Datei 4.mp3 abgespielt. Diese Datei muss auf der eingesteckten SD-Karte gespeichert sein und enthält einen Monsterschrei.
Um den Eindruck zu erwecken, dass sich ein Monster in der Kiste befindet, leuchten außerdem zwei rote LEDs auf, die durch ein Loch in der Kiste sichtbar sind.
Wichtiges zur Tonausgabe: Die Wiedergabe des Tones beginnt mit der jeweiligen Anweisung und der nächste Code wird ausgeführt. Der Ton wird jedoch weiterhin bis zum Ende abgespielt, während der Mikrocontroller die nächsten Anweisungen ausführt.
if (distance < 200 && distance >= 150) { myDFPlayer.volume(15); myDFPlayer.play(4); digitalWrite (leds_static_eyes, HIGH); delay(6000); digitalWrite (leds_static_eyes, LOW); delay(3000); }
Wenn sich ein Objekt in einer Entfernung von weniger als 150 cm oder mehr als 100 cm befindet, wird wieder die Lautstärke verändert und anschließend die Datei 2.mp3 abgespielt.
Außerdem verwenden wir zwei Servomotoren an den Seiten der Kiste, als ob sich etwas im Inneren bewegt und dagegen stoßen würde. Um dies zu erreichen, ändern wir den Positionsparameter des Servomotors 0, der sich auf der linken Seite der Kiste befindet, und des Servomotors 4, der sich auf der rechten Seite befindet. Wir verwenden eine `for`-Schleife, die die Bewegungen insgesamt fünfmal durchführt. Innerhalb der Schleife ändern wir die Positionen der beiden Servomotoren. Nach jeder neuen Positionsänderung erfolgt eine Wartezeit von 200 ms. Nach Abschluss der fünf Durchläufe wird die Schleife beendet und es erfolgt eine Wartezeit von 5 Sekunden.
if (distance < 150 && distance > 100) { myDFPlayer.volume(15); myDFPlayer.play(2); for (int n=0; n<5; n +=1) { servoDriver_module.setPWM(0, 0, 120); delay(200); servoDriver_module.setPWM(0, 0, 180); delay(200); servoDriver_module.setPWM(4, 0, 230); delay(200); servoDriver_module.setPWM(4, 0, 170); delay(200); } delay(5000); }
Wenn die gemessene Entfernung weniger oder gleich 100 cm und größer als 50 cm ist, wird erneut die Lautstärke verändert und dieses Mal die Datei 1.mp3 abgespielt.
Sobald der Ton abgespielt wird, öffnet sich langsam der Deckel, als würde das Monster hervorschauen. Es werden zwei LEDs als Augen leuchten und sich ein paar Mal seitwärts bewegen, bevor es sich wieder schnell versteckt. In einer Schleife wird die Position des zugehörigen Servos Nr. 1 verändert. Nach der Änderung der Servoposition, werden die Zustände der LED-Pins verändert. Wenn sie leuchten, sollen sie sich seitlich bewegen. Mit einer weiteren Schleife wird die Position des dazugehörigen Servos Nr. 2 verändert. Das läuft zwei Mal durch. Anschließend werden alle Servos auf die Grundposition zurückgestellt und die LEDs ausgeschaltet.
if (distance <= 100 && distance > 50) { myDFPlayer.volume(15); myDFPlayer.play(1); for (int pos=400; pos>160; pos -=2) { servoDriver_module.setPWM(1, 0, pos); delay(10); } digitalWrite (servo_leds_eyes, HIGH); delay(1000); for (int n=0; n<2; n +=1) { servoDriver_module.setPWM(2, 0, 125); delay(1000); servoDriver_module.setPWM(2, 0, 275); delay(1000); } servoDriver_module.setPWM(2, 0, 200); delay(1000); servoDriver_module.setPWM(1, 0, 400); delay(50); digitalWrite (servo_leds_eyes, LOW); delay(5000); }
Ist der gemessene Abstand kleiner als 50 cm, wird auch hier die Lautstärke verändert und die Datei 2.mp3 abgespielt.
Nun möchten wir, dass das Monster eine Klaue durch das untere rechte Loch der Kiste schiebt. Um dies zu erreichen, verwenden wir die Bewegung des Servomotors 3, an dem wir die Klaue befestigt haben. Wenn die Klaue aus der Kiste herausragt, werden die statischen LEDs wie Augen leuchten. Das wird drei Mal wiederholt. Die Klaue zieht sich ein Stück zurück und wird dann wieder ganz nach außen positioniert. Diese Bewegung wird auch drei Mal durchgeführt. Danach wird die Position des Servos zurückgesetzt und die LEDs werden ausgeschaltet.
if (distance <= 50) { myDFPlayer.volume(15); myDFPlayer.play(2); servoDriver_module.setPWM(3, 0, 100); digitalWrite (leds_static_eyes, HIGH); delay(1000); for (int n=0; n<3; n +=1) { servoDriver_module.setPWM(3, 0, 150); delay(500); servoDriver_module.setPWM(3, 0, 100); delay(500); } servoDriver_module.setPWM(3, 0, 230); digitalWrite (leds_static_eyes, LOW); delay(5000); }
Die Methode home() wird verwendet, um alle Servomotoren in ihre Ausgangspositionen zu bringen. Sie wird bei Programmstart im setup() ausgeführt, um sicherzustellen, dass alle Komponenten aus einer bekannten Position starten.
void home() { servoDriver_module.setPWM(0, 0, 180); servoDriver_module.setPWM(1, 0, 400); servoDriver_module.setPWM(2, 0, 200); servoDriver_module.setPWM(3, 0, 230); servoDriver_module.setPWM(4, 0, 170); delay(1000); }
Sketch als Download: the_box_monster.ino
Wir hoffen, dass Sie eine gruselig unterhaltsame Halloween-Nacht und Spaß beim Bau dieses Projekts haben.
Happy Halloween.
Hier noch die Links zu den Halloween-Projekten der letzten Jahre:
Der Leser Sascha Stiefenhofer hat das Projekt für den 3D-Drucker aufbereitet und via Thingiverse zur Verfügung gestellt, vielen Dank an dieser Stelle:
Quelle: Thingiverse
Der Leser Lars hat uns seine Bilder der Box zur Verfügung gestellt, auch dafür vielen Dank:
Der Leser Philipp hat ebenfalls sein Projekt via Thingiverse als 3D-Modell zur Verfügung gestellt:
Quelle: thingiverse
]]>
Dass ESP32 und ESP8266 selbständig E-Mails versenden können, habe ich kürzlich in einer Beitragsreihe gezeigt (Teil 1, Teil 2). Dafür ist ein Mail-Konto bei einem Provider wie Gmail erforderlich, welches eine entsprechende Schnittstelle zur Verfügung stellt. In diesem und drei weiteren Posts stelle ich, im Zusammenwirken mit verschiedenen Schaltungen, vier weitere Möglichkeiten des Nachrichtenversands vor. Ich beginne heute mit IFTTT. Das ist das Akronym für If This Then That. Hinter diesem Namen steht ein Web-Portal, das diverse Dienste zur Verfügung stellt. Unter anderem den Versand von E-Mails, getriggert durch einen Post von einem ESP32 oder ESP8266. Dazu brauchen wir einen Account bei IFTTT. Pro Konto kann man zwei Anwendungen kostenlos erstellen. Wie das funktioniert, beschreibe ich in dieser Folge aus der Reihe
heute
Die heutige Schaltung stellt einen Personenzähler dar, der über Ultraschall Leute zählen kann, die einen Saal oder ein Zimmer betreten. Den Sensor, einen HC-SR04, habe ich so an der Tür angebracht, dass sich bis zur passierenden Person ein Abstand von ca. 30 cm ergibt. Das nächste Hindernis sollte dann einen Abstand von einem Meter oder mehr haben. Die Grenzwerte lassen sich natürlich im Programm an die bestehenden örtlichen Verhältnisse anpassen. Der Zählvorgang passiert, wenn die Person den Schallkegel verlässt. Eine Hysterese, also ein Ausschlussbereich von möglichen Entfernungen, stellt sicher, dass Mehrfachzählungen unterbunden werden. Der Controller wird getriggert, wenn eine Entfernung von weniger als 30 cm gemessen wird. Erst wenn der Schallweg dann wieder mehr als einen Meter beträgt, wird der Zähler um eins erhöht.
Abbildung 1: Anbringen des Sensors
Durch die Vorgabe eines Zählwert-Limits kann gesteuert werden, wann der Controller die IFTTT-Anwendung triggern soll. Kurze Zeit später trifft die Mail dann in dem angegebenen Konto ein.
Jeder der angeführten Controller-Typen ist grundsätzlich einsetzbar, zumindest in diesem Beitrag. Bei Verwendung eines BME280 scheidet der ESP8266 allerdings aus, wegen Speicherplatzmangels. Deshalb habe ich hier einen SHT21 für die Temperaturmessung eingesetzt. Ein weiterer Grund für diese Entscheidung ist, dass der Baustein, wie auch das Display, über den I2C-Bus ansteuerbar ist.
Um den Zustand der Schaltung jederzeit auch direkt vor Ort einsehen zu können, habe ich dem ESP ein kleines Display spendiert. Über die Flash-Taste ist ein geordneter Abbruch des Programms möglich, falls zum Beispiel Aktoren sicher ausgeschaltet werden müssen, wie hier die LED. Dem ESP8266 D1 mini muss man dazu ein extra Tastenmodul spendieren, weil er selbst keine Flash-Taste hat.
1 |
D1 Mini NodeMcu mit ESP8266-12F WLAN Modul oder D1 Mini V3 NodeMCU mit ESP8266-12F oder NodeMCU Lua Amica Modul V2 ESP8266 ESP-12F WIFI oder NodeMCU Lua Lolin V3 Module ESP8266 ESP-12F WIFI oder ESP32 Dev Kit C unverlötet oder ESP32 Dev Kit C V4 unverlötet oder ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder NodeMCU-ESP-32S-Kit oder |
1 |
|
1 |
LED, zum Beispiel rot |
1 |
Widerstand 330 Ω |
1 |
Widerstand 1,0 kΩ |
1 |
Widerstand 2,2 kΩ |
2 |
|
1 |
|
diverse |
Jumper Wire Kabel 3 x 40 STK. je 20 cm M2M/ F2M / F2F evtl. auch |
optional |
Damit neben dem Controller noch Steckplätze für die Kabel frei sind, habe ich zwei Breadboards, mit einer Stromschiene dazwischen, zusammengesteckt.
Abbildung 2: Entfernungsmesser mit Ultraschall - ESP8266
Abbildung 3: Entfernungsmesser mit Ultraschall - ESP32
Abschließend zur Hardware kommen jetzt noch die Schaltbilder für ESP32 und ESP8266.
Abbildung 4: Schaltung für ESP32
Abbildung 5: Schaltung für ESP8266
Thonny oder
ssd1306.py Hardwaretreiber für das OLED-Display
oled.py API für das OLED-Display
sht21.py Treiber für das SHT21-Modul
urequests.py Treibermodul für den HTTP-Betrieb
ifttt.py Demoprogramm für den e-Mailversand
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 18.06.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Folgen Sie dem Link zu ifttt.com und klicken Sie auf Get started rechts oben.
Abbildung 6: IFTTT-Startseite © ifttt.com
Ich möchte mich nicht über ein Google-Konto anmelden sondern mit meiner e-Mailadresse – sign up.
Abbildung 7: Sign up - Anmelden oder Registrieren © ifttt.com
Registrieren mit Mailadresse und einem Passwort. Es sollte nicht das sein, mit dem Sie sich auf dem Mailserver einloggen. Aufpassen! Das Passwort wird nur einmal eingegeben und nicht verifiziert!
Abbildung 8: Registrieren © ifttt.com
Abbildung 9: optional - Handy-App herunterladen © ifttt.com
Mindestens eines der Felder muss man anklicken, sonst geht's nicht weiter.
Abbildung 10: Mindestens ein Feld muss markiert werden © ifttt.com
Wir brauchen für unseren Zweck keinen Vollzugriff, deshalb – Not now.
Abbildung 11: Wir wollen keinen Pro-Account © ifttt.com
Jetzt ganz nach unten scrollen – Get startet
Abbildung 12: Get started - Legen wir los © ifttt.com
In unserem Account legen wir jetzt ein Applet an. Starten Sie auf der Hauptseite mit Create.
Abbildung 13: Wir starten mit Create © ifttt.com
Zuerst muss ein Trigger, also ein Auslöser, definiert werden. Klick auf Add.
Abbildung 14: Wir erzeugen einen Trigger © ifttt.com
Geben Sie webhook in das Suchfeld ein und klicken Sie dann auf das Symbol Webhooks.
Abbildung 15: Ein Webhook wird benötigt © ifttt.com
Wir werden eine Web-Anfrage senden, checken Sie das mittlere Feld.
Abbildung 16: Receive a webrequest © ifttt.com
Geben Sie einen Namen für die App ein und klicken Sie auf Create Trigger.
Abbildung 17: Trigger generieren © ifttt.com
Damit ist die Erzeugung des Webhooks abgeschlossen. Klicken Sie jetzt auf Then und schreiben Sie email in das Suchfeld. Dann klicken Sie auf das linke Symbol - Email.
Abbildung 18: Einen Dienst auswählen © ifttt.com
Verbinden Sie jetzt den Service mit dem Trigger – Connect.
Abbildung 19: Service verbinden © ifttt.com
Als Nächstes wird die Empfängeradresse der Mails eingetragen. An dieses Konto verschickt IFTTT gleich eine E-Mail mit einer Pin, die ins nächste Feld eingetragen werden muss.
Abbildung 20: Mailempfänger angeben © ifttt.com
Schauen Sie im Mail-Konto nach. Dort sollte eine Mail von IFTTT angekommen sein. Übertragen Sie die Pin in das Formular und klicken Sie auf Connect.
Abbildung 21: Postfach öffnen und Pin übertragen
Nun werden der Betreff und der Text der Mail editiert – Create action.
Abbildung 22: Inhalt der Mail editieren © ifttt.com
Mit Continue gelangen Sie zur nächsten Seite mit der Übersicht der App.
Abbildung 23: Zusammenfassung © ifttt.com
Abbildung 24: Bestätigung © ifttt.com
Gehen Sie jetzt auf maker_webhooks – Documentation.
Abbildung 25: Zur Dokumentation © ifttt.com
Auf dieser Seite finden Sie den 22-stelligen API-Key, den Sie sich notieren oder besser kopieren sollten, denn sie brauchen ihn später. Füllen Sie im Formular die Felder aus und klicken Sie auf Test it, um eine Testmail zu versenden.
Abbildung 26: Test-Mail versenden © ifttt.com
Haben Sie die Mail bekommen?
Die Anschlüsse auf dem ESP32 sind so gewählt, dass die Nummern auch für den ESP8266 gelten. Somit arbeitet das Programm für beide Controllerfamilien ohne Änderungen. Es beginnt mit einer Übersetzungstabelle für den ESP8266. Diese Familie ärgert die User gerne mit ständigen Neustarts. Deshalb sollte nach dem Flashen der Firmware als erstes webrepl „getötet“ werden.
# ifttt.py
#
# Pintranslator fuer ESP8266-Boards
# LUA-Pins D0 D1 D2 D3 D4 D5 D6 D7 D8
# ESP8266 Pins 16 5 4 0 2 14 12 13 15
# SC SD T
#
# Nach dem Flashen der Firmware auf dem ESP8266:
# import webrepl_setup
# > d fuer disable
# Dann RST; Neustart!
Dann erledigen wir das Importgeschäft. Von machine holen wir die Klassen Pin und SoftI2C, von time die Funktion sleep. Timeout ist ein Eigenbau-Modul, das drei nichtblockierende Softwaretimer enthält. Von den Funktionen wird eine Funktion zurückgegeben, eine sogenannte Closure. Indem ich mit dem * alles von dem Modul in den globalen Namensraum einbinde, kann ich später auf die Funktionen so zugreifen, als ob sie direkt im Programm deklariert worden wären, also ohne das sonst übliche Objekt-Prefix.
Die Klasse SHT21 bedient das SHT21-Platinchen, die Klasse OLED stellt ein API für das Display zur Verfügung. Sie benötigt die Klasse SSD1306, weshalb die Datei ssd1306.py auch in den Flash des Controllers hochgeladen werden muss. Weil beim ESP8266 das Modul urequests nicht im Kernel enthalten ist, muss auch dieses hochgeladen werden. Der ESP32-Kernel enthält das Modul bereits. Es dient zum Senden und Empfangen von HTML-Paketen. Für die Anbindung an das lokale Netzwerk brauchen wir network. sys liefert uns Möglichkeiten, den Controllertyp abzufragen und das Programm geordnet zu verlassen.
from machine import Pin, SoftI2C
from time import sleep
from timeout import *
from sht21 import SHT21
from oled import OLED
import urequests as requests
import network
import sys
Im nächsten Abschnitt setzen wir die URL für den HTTP-Request zusammen und geben die Credentials für den Router-Zugriff an.
iftttKey="here goes your key"
iftttApp="person_passed"
iftttUrl='http://maker.ifttt.com/trigger/'+\
iftttApp+'/with/key/'+iftttKey
mySid = 'EMPIRE_OF_ANTS'; myPass = "nightingale"
Über die Variable sys.platform ermitteln wir den Controllertyp, stellen dementsprechend die GPIO-Pins für den I2C-Bus ein und erzeugen ein Bus-Objekt.
if sys.platform == "esp8266":
i2c=SoftI2C(scl=Pin(5),sda=Pin(4))
elif sys.platform == "esp32":
i2c=SoftI2C(scl=Pin(22),sda=Pin(21))
else:
raise RuntimeError("Unknown Port")
Das I2C-Bus-Objekt reichen wir an die Konstruktoren des OLED-Displays und des SHT21 weiter.
d=OLED(i2c,heightw=32) # 128x32-Pixel-Display
d.clearAll()
d.writeAt("PERSON COUNTER",0,0)
sht=SHT21(i2c)
Dann deklarieren wir das Tastenobjekt, den GPIO-Ausgang für die LED, den Triggerausgang für den HC-SR04 und den Eingang für das Laufzeitsignal des Ultraschallsensors. Der Ultraschallgeber wir durch eine fallende Flanke an GPIO14 getriggert. Er sendet dann nach ca. 250ms einen 40kHz-Burst mit 200ms Dauer aus. Danach geht der Echo-Ausgang unmittelbar auf HIGH-Pegel. Wird dann ein Echo empfangen, fällt der Ausgangspegel auf LOW. Der Controller muss die Laufzeit ermitteln und durch 2 teilen, weil der Weg ja zweimal durchlaufen wird.
taste=Pin(0,Pin.IN,Pin.PULL_UP)
red=Pin(2,Pin.OUT,value=0)
trigger=Pin(14,Pin.OUT, value=1)
echo=Pin(13,Pin.IN)
limit=10
temperatur=20
Nachdem der Zähler 10 erreicht hat, wird eine Mail getriggert. Vorab deklarieren wir schon einmal die Variable temperatur, weil die Schallgeschwindigkeit Temperaturabhängig ist, muss sie berücksichtigt werden. Der SHT21 sagt sie uns später.
Das Dictionary connectstatus übersetzt die numerischen Werte für den Verbindungsstatus in Klartext.
connectStatus = {
1000: "STAT_IDLE",
1001: "STAT_CONNECTING",
1010: "STAT_GOT_IP",
202: "STAT_WRONG_PASSWORD",
201: "NO AP FOUND",
5: "UNKNOWN",
0: "STAT_IDLE",
1: "STAT_CONNECTING",
5: "STAT_GOT_IP",
2: "STAT_WRONG_PASSWORD",
3: "NO AP FOUND",
4: "STAT_CONNECT_FAIL",
}
Mit der Funktion hexMac() erfahren wir die MAC-Adresse des Station-Interfaces des Controllers im Klartext. Diese muss im Router eingetragen werden, sonst verweigert dieser dem Controller den Zugang. Meistens geschieht der Eintrag über das Menü WLAN – Sicherheit. Das genaue Vorgehen verrät das Handbuch des Routers.
# ********** Funktionen definieren ***********
def hexMac(byteMac):
"""
Die Funktion hexMAC nimmt die MAC-Adresse im Bytecode
entgegen und bildet daraus einen String fuer die Rueckgabe
"""
macString =""
for i in range(0,len(byteMac)): # Fuer alle Bytewerte
macString += hex(byteMac[i])[2:] # ab Position 2 bis Ende
if i <len(byteMac)-1 : # Trennzeichen
macString +="-"
return macString
Wir tasten das Bytes-Objekt, das uns der Funktionsaufruf nic.config('mac') liefert, Zeichen für Zeichen ab, machen daraus eine Hexadezimalzahl und bauen daraus den String auf, den die Funktion zurückgibt.
>>> nic.config('mac')
b'\xf0\x08\xd1\xd1m\x14'
>>> hex(m[0])
'0xf0'
>>> hex(m[0]) [2:]
'0xf0'
>>> hex(m[0])[2:]
'f0'
Die steigende Flanke an echo triggert einen Interrupt, also eine Programmunterbrechung. Das Programm kann gerade irgendwo sein, wenn der Pegel an GPIO13 steigt. Jetzt muss jedenfalls erst einmal die Systemzeit in Microsekunden genommen werden. Weil eine ISR (aka Interrupt Service Routine) keinen Wert zurückgeben kann wie eine normale Funktion, muss die Zeitmarke über eine globale Variable, hier start, an das Hauptprogramm gemeldet werden. Das ist nur möglich, wenn die Variable in der Funktion als global deklariert wird.
Dann setzen wir für den nächsten Durchlauf den Triggerausgang wieder auf HIGH und ändern den Handler so ab, dass der Eingang auf die fallende Flanke des Echo-impulses reagieren kann. Der Handler ist die Routine, die im Falle der Unterbrechungsanforderung ausgeführt werden soll. Zuletzt wird noch die LED eingeschaltet. Das reicht, mehr sollte man hier nicht reinpacken, ISRs sollen möglichst kurzgehalten werden.
def startZeit(pin):
global start
start=ticks_us()
trigger.on()
echo.irq(handler=stoppZeit,trigger=Pin.IRQ_FALLING)
red.on()
Der nächste Interrupt, ausgelöst durch die fallende Flanke des Echo-Impulses, wird durch stoppZeit() bedient. Die global deklarierten Variablen ende und fertig liefern die gewünschten Informationen ans Hauptprogramm zurück. Die IRQ-Behandlung wird deaktiviert und die LED ausgeschaltet.
def stoppZeit(pin):
global ende, fertig
ende=ticks_us()
fertig=True
echo.irq(handler=None,trigger=Pin.IRQ_RISING)
red.off()
Die Funktion entfernung() berechnet aus dem übergebenen Argument in runtime die Entfernung mit Hilfe der Schallgeschwindigkeit. Dabei wird berücksichtigt, dass die Schallgeschwindigkeit mit der Temperatur ansteigt und zwar um 0,6m/s. Sofern die Laufzeit weniger als 15ms (entspricht 2,56m @20°C) beträgt, ist der Messwert weitgehend zuverlässig. Bei mehr als 15ms Laufzeit wird 999 als Fehlercode zurückgegeben.
def entfernung(runtime):
if runtime <= 15:
return int((331.5 + 0.6*temperatur)*runtime/2)
else:
return 999
Mit dem Aufruf der Funktion messen() wird ein Messvorgang getriggert. Wieder werden Variablen als global bekanntgemacht. fertig setzen wir auf False und stellen für die Bedienung der Unterbrechungsanforderung (IRQ = Interrupt Request) als ISR startZeit() ein. Mit trigger.off() geht der Ausgang auf LOW, und der HC-SR05 beginnt seinen Zyklus. Der Timer expired() wird auf 200ms gestellt. Er bricht den Messvorgang ab, wenn der HC-SR04 kein Echosignal empfängt. Dass ein Messvorgang gestartet wurde, sagen wir der Hauptschleife mit der Variablen triggered.
In der Funktion senden() senden wir eine Nachricht an IFTTT und nehmen die Antwort des Servers entgegen. Die Nachricht mit der URL wird mit der Methode POST abgeschickt. Mit im Gepäck befindet sich das Dictionary {'value1': str(cnt)}, das mit dem Parameter json überreicht wird. In gleicher Weise wird der Headertyp übergeben. Über den json-Code werden Sie in einer späteren Folge noch etwas erfahren.
def senden(cnt):
resp = requests.post(iftttUrl,
json={'value1': str(cnt)},
headers={'Content-Type': 'application/json'})
if resp.status_code == 200:
print("Transfer OK")
else:
print("Transfer-Fehler")
resp.close()
gc.collect()
requests.post() gibt ein response-Objekt zurück. Im Attribut status_code befindet sich der Rückgabe-Code des Servers, 200 bedeutet fehlerfrei angekommen. resp.close() schließt die Unterhaltung und gc.collect() räumt den Speicher auf.
Der Controller stellt jetzt die Verbindung mit dem Router her. Dazu wird erst einmal das Accesspoint-Interface ausgeschaltet, wenn es denn überhaupt an war. Das ist beim ESP8266 manchmal nötig. Dann schalten wir das Station-Interface ein, der Controller arbeitet ja als Client. Die Schaltsekunde danach ist wichtig und vermeidet interne WLAN-Fehler.
# ********************* Bootsequenz ************************
#
nic=network.WLAN(network.AP_IF)
nic.active(False)
nic = network.WLAN(network.STA_IF) # erzeugt WiFi-Objekt nic
nic.active(True) # nic einschalten
sleep(1)
MAC = nic.config('mac') # binaere MAC-Adresse abrufen und
myMac=hexMac(MAC) # in eine Hexziffernfolge umgewandelt
print("STATION MAC: \t"+myMac+"\n") # ausgeben
Wir lesen die MAC-Adresse aus, lassen sie in Klartext umformen und im Terminal ausgeben.
if not nic.isconnected():
nic.connect(mySid, myPass)
print("Status: ", nic.isconnected())
d.writeAt("WLAN connecting",0,1)
points="............"
n=1
while nic.status() != network.STAT_GOT_IP:
print(".",end='')
d.writeAt(points[0:n],0,2)
n+=1
sleep(1)
Wenn die Schnittstelle noch keine Verbindung zum Router hat, stellen wir mit connect() eine her. Dabei werden SSID und Passwort übertragen. Der Status wird abgefragt und ausgegeben. Solange wir vom DHCP-Server auf dem Router noch keine IP-Adresse bekommen haben, wird im Sekundenabstand ein Punkt ausgegeben. Das sollte nicht länger als 4 bis 5 Sekunden dauern.
Dann fragen wir den Status ab und lassen uns die Verbindungsdaten, IP-Adresse, Netzwerkmaske und Gateway, mitteilen.
Ein paar Startwerte werden gesetzt, bevor es in die Hauptschleife geht.
triggered=False
ende=start=0
alt=neu = 0
personen=0
expired=TimeOutMs(0)
nextScan=TimeOutMs(20)
messen()
count=0
In der Hauptschleife warten wir auf das Eintreten verschiedener Ereignisse. Wurde eine Messung getriggert, aber kein Echo festgestellt, dann muss die Messung als fehlerhaft abgebrochen werden. Wir setzen runtime, ende und start auf 0, schalten die IRQ-Behandlung zuerst definitiv aus und dann auf Start. Die Triggerleitung legen wir auf HIGH und setzen fertig auf False.
if triggered and expired():
runtime=ende=start=0
echo.irq(handler=None,trigger=Pin.IRQ_RISING)
echo.irq(handler=startZeit,trigger=Pin.IRQ_RISING)
trigger.on()
fertig=False
Wenn eine Messung fertig ist, muss festgestellt werden, ob der Entfernungswert im richtigen Bereich liegt. Zuerst setzen wir aber fertig auf False und berechnen dann die Laufzeit in Millisekunden. Diesen Wert geben wir an die Funktion entfernung() weiter, die uns die Distanz zum Messobjekt liefert. In der Testphase können wir die Werte im Display ausgeben lassen.
if fertig:
fertig=False
runtime = (ende - start)/1000
distance=entfernung(runtime)
# print("runtime",runtime, "distance",distance)
if distance < 300: # Person im Erfassungskegel
neu=1
elif distance > 700:
neu=0
triggered=False
Ist die Distanz kleiner als 30cm, dann befindet sich eine Person im Schallkegel. Verlässt sie diesen, setzen wir ab 70cm die Variable neu auf 0.
Nur wenn die Person den Schallkegel verlassen hat, zählen wir um eins weiter. Ist limit erreicht, triggern wir eine E-Mail von IFTTT und setzen limit um 10 höher. Stets lassen wir uns die Personenanzahl und das aktuelle Limit im Terminal und am Display ausgeben.
if alt==1 and neu==0:
count+=1
if count == limit:
senden(count)
limit+=10
# print(count, limit,)
d.clearFT(0,1,15,2,False)
d.writeAt("Personen:{}".format(count),0,1,False)
d.writeAt("Limit: {}".format(limit),0,2)
Danach setzen wir grundsätzlich alt auf neu.
Mit dem Timer nextScan() können wir die zeitliche Abfolge von Messvorgängen steuern. Das Messintervall muss länger als 20ms sein. Wir lesen die Rohtemperaturwerte des SHT21 ein und lassen diese in die aktuelle Celsius-Temperatur umrechnen. Danach triggern wir eine neue Messung und starten den Timer neu.
if nextScan():
sht.readTemperatureRaw()
temperatur=sht.calcTemperature()
messen()
nextScan=TimeOutMs(100)
Das letzte Ereignis, das eintreten kann, ist eine gedrückte Abbruchtaste. In diesem Fall schalten wir die LED aus und beenden das Programm.
Im Produktionsbetrieb muss der Controller autonom ohne PC und USB-Kabel starten. Damit er das kann, speichern Sie nach beendeter Testphase das Programm ifttt.py als main.py direkt im Flash des Controllers ab. Das erreichen Sie am schnellsten mit Strg + Shift +S, im Dialogfenster wählen Sie MicroPython device.
Abbildung 27: Aufbau mit ESP8266
Mit IFTTT können Sie nur reine Text-Nachrichten bekommen. In der nächsten Folge zeige ich Ihnen, wie man eine grafische Aufbereitung von Messwerten erhalten kann. Wir werden einen BME280 für Messung von Temperatur, Luftdruck und relativer Luftfeuchte benutzen, sowie einen BH1750 zur Ermittlung der Helligkeit.
In einer weiteren Folge werden wir interaktiv. Über Telegram steuern wir einen ESP32 und fragen zum einen wieder dieselben Sensoren ab, zum anderen schalten wir eine LED über den Bot.
Wer statt Telegram lieber Whatsapp benutzen möchte, kann sich Messwerte und Zustände auch auf diesem Weg zustellen lassen. Wie das geht, erkläre ich in der letzten Folge zum Thema Messaging.
Ganz nebenbei erfahren Sie auch etwas über die Ähnlichkeit zwischen MicroPythons Dictionarys und dem JSON-Code, sowie über Einsatz von Callback-Routinen.
Neugierig geworden? Dann bleiben Sie dran!
]]>Zum Blog bieten wir kurzzeitig ein Radiowecker Produktset an, mit dem Sie die Produkte im Vergleich zu den Einzelpreisen deutlich günstiger erhalten.
Anzahl | Bauteil | Anmerkung |
---|---|---|
1 |
AZ-Touch mit 2,4 Zoll Display oder 2,8 Zoll Display |
|
1 |
|
|
2 |
|
|
1 |
|
|
1 |
|
|
1 |
|
|
1 |
Optional |
Bis zum 17. Oktober war hier ein fehlerhafter Schaltplan abgebildet. Das gilt auch für das PDF zum Beitrag. Wenn Sie das PDF vorher heruntergeladen haben, sollten Sie es erneut herunterladen!
Der Schaltplan zeigt nur jene Teile, die im AZ-Touch nicht vorhanden sind. Die folgenden Abbildungen zeigen, wie man die Platine des AZ-Touch entsprechend erweitert. Für die Audioverstärker werden zwei 7-polige Federleisten zum Aufstecken der Audioverstärker auf dem Lochrasterfeld des AZ-Touch angebracht. Ebenso ein 470 kΩ Widerstand. Wer den optionalen Lichtsensor verwenden will, sollte eine zweipolige Stiftleiste bei 3,3 V und GND, sowie eine einpolige bei A0 einlöten.
ACHTUNG: Der LDR R2 muss zwischen GPIO36 und 3.3V, der Widerstand R3 zwischen GPIO36 und GND angeschlossen werden. In einer älteren Variante des Schaltplans war dies andersherum dargestellt. Bei fdalschem Anschluss wird die Hintergrundbeleuchtung bei Dunkelheit heller.
Die zweite Abbildung zeigt, wie die Verdrahtung auf der Rückseite erfolgen sollte. In Gelb sind die Konturen der Federleisten und des Widerstands dargestellt. Der Anschluss des LDR Moduls ist wie dargestellt richtig. Der Pin mit dem Aufdruck "-" gehört an 3.3V !
Zusammenbau
Zur Unterbringung der Lautsprecher gibt es für den AZ-Touch eine Rückwand, die ihn in ein Pultgehäuse verwandelt. Die Rückwand kann mit einem 3D-Drucker hergestellt werden. Sie hat auch eine Öffnung für Die DC-Einbaubuchse. Datei zum Drucken der Rückwand.
Zum Einbau des optionalen LDR gibt es eine Befestigungsplatte, auf die das LDR-Modul geschraubt werden kann. Oben im Gehäuse wird ein 5mm Loch gebohrt und dann die Befestigungsplatte mit doppelseitigem Klebeband so an die Rückwand geklebt, dass der LDR in die Bohrung zu liegen kommt.
Der Sketch wurde aus Gründen der Übersichtlichkeit in mehrere Teile zerlegt. Dazu wird eine Funktion genutzt, die die Arduino IDE zur Verfügung stellt. Gibt es neben dem Hauptsketch, der denselben Namen wie der Ordner hat, noch weitere „.ino“ oder „.h“ Dateien im selben Ordner, so werden diese vom Compiler in alphabetischer Reihenfolge an den Hauptsketch angehängt.
Da der gesamte Code sehr umfangreich geworden ist, gibt es diesen nur zum Herunterladen.
Die ZIP-Datei enthält den Ordner mit allen zugehörigen Dateien. Sie muss in den Ordner der Projektdateien (oft Dokumente\Arduino\) entpackt werden. Im Folgenden werden die einzelnen Teile kurz beschrieben. Eine detaillierte Beschreibung finden Sie als Kommentare im Code.
Damit der Sketch kompiliert werden kann, muss die Arduino IDE entsprechend vorbereitet werden. Die Arduino IDE unterstützt standardmäßig eine große Anzahl von Boards mit unterschiedlichen Mikrocontrollern, nicht aber den ESP32. Damit man Programme für diese Controller erstellen und hochladen kann, muss daher ein Softwarepaket für die Unterstützung installiert werden.
Zuerst müssen Sie der Arduino IDE mitteilen, wo sie die zusätzlich benötigten Daten findet. Dazu öffnen Sie im Menü Datei den Punkt Voreinstellungen. Im Voreinstellungs-Fenster gibt es das Eingabefeld mit der Bezeichnung „Zusätzliche Boardverwalter URLs“. Wenn Sie auf das Icon rechts neben dem Eingabefeld klicken, öffnet sich ein Fenster, in dem Sie die URL https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json eingeben können.
Nun wählen Sie in der Arduino IDE unter Werkzeug → Board die Boardverwaltung.
Es öffnet sich ein Fenster, in dem alle zur Verfügung stehenden Pakete aufgelistet werden. Um die Liste einzugrenzen, gibt man im Suchfeld „esp32“ ein. Dann erhält man nur noch einen Eintrag in der Liste. Installieren Sie das Paket „esp32“. Falls das Paket schon installiert war, prüfen Sie bitte, ob Sie die Version 2.0.9 haben.
Für das Display benötigen Sie zwei Bibliothek, die über die Arduino Bibliotheksverwaltung installiert werden können. Das ist die Bibliothek „Adafruit_ILI9341“ in der Version 1.5.10
und die Bibliothek „Adafruit_GFX“ in der Version 1.10.14.
Zwei weitere Bibliotheken werden für den Touchscreen benötigt. Das ist „XPT2046_Touchscreen“ in der Version 1.4.0
und „Touchevent“ in der Version 1.3.0
Kernstück dieses Projekts ist aber die Bibliothek „ESP8266Audio“ von Earle F. Philhower in der Version 1.9.7.
Diese Bibliothek ermöglicht es, verschiedene digitale Audiostreams zu lesen, zu dekodieren und über verschiedene Ausgangskanäle wiederzugeben. Als Eingang, kann der Programmspeicher, der interne RAM, ein Filesystem, eine SD-Karte, ein HTTP-Stream, oder ein ICY-Stream genutzt werden. Der ICY-Stream wird typisch von Internet-Radios genutzt.
Dekodiert werden können WAV, MOD, MIDI, FLAC, AAC und MP3 Dateien. Für das Webradio wird MP3 benötigt. Die Ausgabe kann schließlich in Speicher, Files oder I2S erfolgen.
Wenn alle Bibliotheken installiert sind, kann der Sketch kompiliert und auf die Hardware hochgeladen werden.
Achtung! Da sich der Sketch aus zahlreichen Teilen zusammensetzt, kann das Kompilieren, insbesondere beim ersten Mal, lange dauern. Für das ESP32 Package und die ESP8266audio Bibliothek ist es wichtig, die angegebenen Versionen zu benutzen, da die Audio-Bibliothek sehr hardwarenahe programmiert wurde.
Wer sich das Kompilieren ersparen will, kann hier die fertig kompilierte Firmware herunterladen und direkt auf den ESP32 hochladen.
Zum Hochladen des Binärfiles benötigt man ein Hilfsprogramm, das bei Espressif kostenlos heruntergeladen werden kann. Flash Download Tools
Sie müssen die ZIP-Datei herunterladen und entpacken. Dann können Sie das enthaltene Programm flash_download_tool_3.9.5.exe starten. Es erscheint ein Fenster zur Auswahl des Controllers.
Hier wählen Sie als Chip-Type ESP32 und als Work-Mode Develop. Mit OK wird das Programm fortgesetzt. Es erscheint das Arbeitsfenster des Tools.
Verbinden Sie den ESP32 über ein USB-Kabel mit dem Computer und wählen Sie unten rechts die verwendete serielle Schnittstelle aus. In der obersten Zeile setzen Sie den Pfad zur heruntergeladenen BIN-Datei für den Radiowecker. Als Zieladresse muss 0x10000 eingegeben werden. Alle anderen Einstellungen wählen Sie wie auf der Abbildung gezeigt. Nun können Sie auf „START“ klicken. Der Upload beginnt. Wenn alles fertig ist, wechselt die grüne Fläche „IDLE“ auf Blau mit dem Text „FINISH“. Der ESP32 hat nun die Firmware für den Radiowecker und die Inbetriebnahme kann erfolgen. Die fertig kompilierte BIN-Datei funktioniert nur mit der aktuellen Version des AZ-Touch.
Bei der ersten Inbetriebnahme sind noch keine Präferenzen vorhanden. Es kann daher keine Verbindung zum lokalen WLAN hergestellt werden. Ein Accesspunkt mit der SSID „radioweckerconf“ ohne Passwort wird gestartet. Auf dem Display erscheint eine entsprechende Meldung.
Mit z.B. einem Smartphone kann nun eine Verbindung zu diesem Accesspoint hergestellt werden. Danach kann in einem Browser über die Adresse 192.168.4.1 die Konfigurationsseite aufgerufen werden.
Nach dem Neustart sollte die Verbindung zum lokalen WLAN erfolgreich hergestellt werden können. Am Display sollte die aktuelle Uhrzeit und das Datum angezeigt werden.
Tippt man irgendwo auf das Display, erscheint die Bedienungsseite.
Es gibt drei Schieberegler für Lautstärke, Helligkeit und Einschlafzeit. Verändert werden die Einstellungen, indem man auf die gewünschte Position tippt. Eine Besonderheit gilt für die Helligkeit. Wird die auf 0 eingestellt, wird, falls der optionale LDR vorhanden ist, die Helligkeit an die Umgebungshelligkeit angepasst.
In der vierten Zeile sieht man die aktuelle Radiostation. Mit den zwei Knöpfen links und rechts können Sender aus der Senderliste ausgewählt werden.
Ganz unten ist eine Reihe von Knöpfen.
Die Wecker-Funktion wird ein- oder ausgeschaltet. Die Anzeige kehrt zur Zeitanzeige zurück. Wurde der Wecker eingeschaltet, erscheint ganz unten im Display der Wochentag und die Uhrzeit, wann der Wecker das nächste Mal ausgelöst wird. Wenn der angezeigte Wochentag und die Uhrzeit zutreffen, wird das Radio automatisch eingeschaltet.
Der ausgewählte Sender wird als aktiver Sender übernommen. Ist das Radio gerade eingeschaltet, so wechselt der Stream automatisch auf den neuen Sender. Die Anzeige kehrt zur Zeitanzeige zurück.
Die Anzeige kehrt zur Zeitanzeige zurück.
Erfolgt 10 Sekunden keine Aktivität, so kehrt die Anzeige automatisch zur Zeitanzeige zurück. Alle Einstellungsänderungen werden in den Präferenzen gespeichert. Die Bedienungsseite wird immer mit voller Helligkeit dargestellt.
Über die URL http://radiowecker/ sollte die Konfigurationsseite abrufbar sein.
Im oberen Teil können die Zugangsdaten und der NTP-Server geändert werden. Die Änderungen werden erst dann wirksam, wenn der Knopf „Speichern“ geklickt wurde.
Mit dem Knopf „Neustart“ kann ein Neustart ausgelöst werden.
Als Nächstes folgen die Weckzeiten. Es können zwei Weckzeiten eingestellt werden. Für jede der Weckzeiten können die Wochentage gewählt werden, an denen die Weckzeiten anzuwenden sind.
Die Dropdown-Liste darunter enthält alle Sender der Senderliste. Auswählbare Sender haben vor dem Namen einen schwarzen Punkt. Im Formular darunter werden die Daten zur ausgewählten Station angezeigt und können geändert werden. Ist das Häkchen bei „Verwenden“ nicht gesetzt, kann die Station im Gerät nicht ausgewählt werden. Da manche URLs nicht funktionieren, sollte eine neue URL mit dem Knopf „Testen“ getestet werden. Ein Klicken auf diesen Knopf startet die Wiedergabe der URL am Gerät. Achtung! Am Gerät muss das Radio zum Testen eingeschaltet sein. Sollte die Wiedergabe nicht funktionieren, wird sofort wieder auf den aktuellen Sender zurückgeschaltet und eine Meldung angezeigt. Ist die Wiedergabe möglich, wird eine Box mit einem Knopf angezeigt. Klicken auf diesen Knopf schließt die Box und beendet den Test. Es wird wieder die aktuelle Station wiedergegeben. Im Eingabefeld „Position“ wird die Position der ausgewählten Station innerhalb der Senderliste angezeigt. Durch eine Änderung dieses Wertes, kann die Station auf die angegebene Position verschoben werden. Mit dem Knopf „Ändern“ können die Änderungen für die ausgewählte Station dauerhaft geändert werden.
Um das Programm zu aktualisieren, ist es nicht notwendig, das Gerät zu öffnen und eine USB-Verbindung herzustellen. In der Arduino IDE sollten Sie bei den Ports den folgenden Eintrag sehen.
Über diesen Port können Sie nun einen Sketch hochladen. Zum Schutz muss nach Aufforderung das Passwort „weckerupdate“ eingegeben werden. Da die Serielle Schnittstelle nicht genutzt werden kann, werden Meldungen am Display angezeigt.
Hier gibt es den Beitrag als PDF
Viel Spaß beim Nachbauen.
Ziffern in rot, gelb, grün und blau
Dieser Download enthält eine neue Variante von 01_ziffern.ino. Wenn die original Datei 01_ziffern.ino durch diese ersetzt wird, kann in der Datei tft_display.ino in Zeile 335, im Befehl drawRGBBitmap ziffern_rot durch ziffern_gelb, ziffern_gruen oder ziffern_blau ersetzt werden.
Ich habe noch eine andere Variante, angeregt durch den Kommentar von kunigunde, die Farbe der Ziffern beliebig zu ändern. Dabei wird statt des RGB-Bitmaps ein monochromes Bitmap benutzt, das dann beliebig eingefärbt werden kann. Sie müssen dazu die Dateien 01_ziffern.ino und tft_display.ino durch die entsprechenden aus dem folgenden Link ersetzen.
In der Datei tft_display.ino in der Funktion updateTime() kann man die Hintergrundfarbe und die Schriftfarbe einstellen.
if ((z<11) && (redraw || (tim[i] != lasttime[i]))) {
tft.fillRect(30+i*55,30,50,70,ILI9341_BLACK); //Hintergrund
tft.drawBitmap(30+i*55,30,ziffern[z],50,70,ILI9341_GREEN); //Schrift
}
Hier als Beispiel grün auf schwarzem Hintergrund.
In der Hauptdatei radiowecker.ino liegt ein Fehler vor, der es verhindert, dass nach einem Verbindungsverlust das Gerät neu gestartet wird.
Die Bedingung in den Zeilen 232 bis 238 muss inklusive "else" Teil entfallen. Sie muss wie folgt aussehen:
getLocalTime(&ti);
minutes = ti.tm_hour * 60 + ti.tm_min;
weekday = ti.tm_wday;
Im letzten Teil hatten wir gesehen, dass man den Raspberry Pi Pico W mit Bluetooth mit dem Smartphone steuern kann. Dazu haben wir einen Code verwendet, der eine „signed Integer“-Zahl (max. 2 hoch 15 -1 = 32767) sendet. Die ersten vier Stellen werden für die Steuerung verwendet, die letzte Stelle können wir für insgesamt drei Schalter/Taster verwenden, um z.B. eine blaue LED blinken zu lassen, eine Sirene einschalten, oder umschalten zwischen Fernsteuerung und autonomem Modus.
Seit Einführung des Pico nehmen die Beispiele und Erklärungen zu PIO (Programmable I/O) einen breiten Raum in der Dokumentation ein. Offensichtlich ein Alleinstellungsmerkmal des Pico gegenüber anderen Mikrocontrollern. Hiermit kann man kleine Programme laufen lassen, ohne die CPU zu belasten. Genau das Richtige für das Blaulicht und die Sirene meines Robot Cars. Aber wie macht man das? Die einfachen Beispiele in der minimalen Assembler-Sprache konnte ich nachvollziehen, aber zwei unabhängige StateMachines in den Code für mein Robot Car einzubauen hat ein Weilchen gedauert. Sie würden diese Zeilen nicht lesen, wenn es am Ende nicht doch noch gelungen wäre.
1 |
Raspberry Pi Pico W mit aktueller Firmware |
1 |
|
alt |
beliebiger Bausatz mit Chassis und Rädern/Motoren |
1 |
|
1 |
Breadboard, Jumperkabel, Batteriekasten mit 4 AA-Batterien |
1 |
LED mit Vorwiderstand, passiver (!) Buzzer |
PC mit Thonny, Android Smartphone/Tablet |
|
Smartphone |
Hier zunächst der Link zu dem Code für die Smartphone App, den man im MIT App Inventor importieren kann, um ihn ggf. für eigene Zwecke anzupassen. Denken Sie daran, dass die UUID (zumindest im Umkreis von mehreren Hundert Metern) „unique=einzigartig“ sein muss, also ggf. neue anfordern und sowohl in der App als auch im MicroPython-Code ändern.
Nach diesen Änderungen kann man entweder die App provisorisch mit dem AI Companion laden oder unter dem Menüpunkt „Build“ als Android App kompilieren und hochladen. (Apple Nutzer bitte im Internet recherchieren, wie das beim iPhone funktioniert).
Screen Shot vor dem Verbinden, |
Screen Shot |
Nun zu den MicroPython-Programmen, die einerseits die Steuerung des Robot Cars und andererseits die neuen Funktionen realisieren müssen. Dabei tasten wir uns anhand der Beispiele der Raspberry Pi-Foundation und ihrer Zeitschriften an das Thema PIO heran. Diese Programmteile ergänzen später das im vorherigen Teil verwendete Programm.
Häufig findet man das PIO-Assembly-Language-Programm für die eingebaute LED (beim Pico ohne W!), also Pin 25.
# Example using PIO to blink an LED and raise an IRQ at 1Hz.
import time
from machine import Pin
import rp2
asm_pio(set_init=rp2.PIO.OUT_LOW) .
def blink_1hz():
# Cycles: 1 + 1 + 6 + 32 * (30 + 1) = 1000
irq(rel(0))
set(pins, 1)
set(x, 31) [5]
label("delay_high")
nop() [29]
jmp(x_dec, "delay_high")
# Cycles: 1 + 1 + 6 + 32 * (30 + 1) = 1000
nop()
set(pins, 0)
set(x, 31) [5]
label("delay_low")
nop() [29]
jmp(x_dec, "delay_low")
# Create the StateMachine with the blink_1hz program, outputting on Pin(25).
sm = rp2.StateMachine(0, blink_1hz, freq=2000, set_base=Pin(25))
# Set the IRQ handler to print the millisecond timestamp.
sm.irq(lambda p: print(time.ticks_ms()))
# Start the StateMachine.
sm.active(1)
Wenig überraschend muss man zu Beginn Module oder Teile davon importieren:
import time
from machine import Pin
import rp2
Dabei ist rp2 ein Mikrocontroller-spezifisches Modul für den Pico, die anderen werden als bekannt vorausgesetzt. Beim Aufruf der Methoden dieses Moduls muss „rp2.“ vorangestellt werden. Alternativ hätte man den folgenden Code verwenden können:
from rp2 import PIO, StateMachine, asm_pio
Als nächstes betrachten wir die Zeilen
asm_pio(set_init=rp2.PIO.OUT_LOW) .
und
sm = rp2.StateMachine(0, blink_1hz, freq=2000, set_base=Pin(25))
Zunächst verwenden wir den @asm_pio-decorator, um MicroPython darüber zu informieren, dass eine Methode in PIO-Assembly-Language geschrieben ist. Wir wollen die Methode set_init verwenden, um einen GPIO als Ausgang zu verwenden. Dabei wird zunächst ein Parameter übergeben, um ihm den Anfangszustand des Pins (LOW) mitzuteilen.
Dann instanziieren wir die StateMachine und übergeben dabei einige Parameter
Die Maschinensprache PIO-Assembly kennt nur wenige Befehle, die wir z.T. in der selbstdefinierten Funktion blink_1hz() erkennen. Jeder Befehl dauert einen Taktzyklus, die Zahlen in eckigen Klammern stehen für weitere angehängte Zyklen. Bei der gewählten Frequenz von 2000Hz sollen jeweils 1000 Zyklen für den ein- und ausgeschalteten Zustand der LED aufgewendet werden, das ermöglicht das Blinken im Sekundentakt.
Der Interrupt Request ( irq(rel(0)) ) wird im Hauptprogramm verwendet, um einen Zeitstempel auszugeben.
Der set-Befehl wird in zweierlei Weise verwendet:
set(pins, 1)
#bzw.
set(pins, 0)
#oder
set(x, 31) [5]
Einmal wird der bei der Instanziierung festgelegte Pin auf 1 (HIGH) bzw. 0 (LOW) gesetzt;
im zweiten Fall wird eines der Scratch-Register mit Namen x auf 31 mit anschließend [5] (Warte-) Zyklen gesetzt. Die Ergänzung [5] wird als „instruction modifier“ bezeichnet (s.u.)
Mit den Anweisungen
label("delay_high") bzw. label("delay_low")
werden Sprungadressen für die Warteschleifen definiert, auf die der Jump-Befehl - jmp(condition, label) - verweist. Dieser wird so lange ausgeführt, bis x gleich 0 wird. Anfangswert war 31 (s.o.), mit jedem Durchgang wird der Wert mit dem ersten Argument jmp(x_dec, "delay_low") um 1 vermindert („decrement“).
An dieser Stelle macht es Sinn, sämtliche Befehle der Maschinensprache PIO-Assembly und die weiteren „Sprungbedingungen“ (condition) sowie zum besseren Verständnis die Register der StateMachine vorzustellen, denn die meisten Bedingungen sind an die Scratch-Register X und Y sowie das Output Shift Register (OSR) geknüpft:
Hier zunächst die neun “instructions” mit kurzer Erklärung:
StateMachine („Zustandsmaschine“) mit Schieberegistern ISR und OSR, Scratch-Registern x und y, dem Clock-Divider, den Program Counter (im Bild mit PC abgekürzt), und der Kontroll-Logik
(Bild: Raspberry Pi Foundation)
Und die Sprungbedingungen für jmp:
Zurück zu unserem Robot Car. Das soll nun ein blaues Blinklicht bekommen, wenn in der APP auf dem Smartphone der erste Button (Schaltfläche oben links) gedrückt wird.
Die drei Buttons haben die Wertigkeiten 1, 2 und 4. Die Summe der eingeschalteten Buttons ergibt die letzte Stelle unseres fünfstelligen Codes. Dieser wird wie folgt in seine Bestandteile zerlegt:
code = str(code)[2:-5] # necessary only for Smartphone APP
code = int(code)
cy = int(code/1000) # digit 1 and 2 # für Vorwärts/Rückwärts
cx = int((code-1000*cy)/10) # digit 3 and 4 # für Links/Rechts
cb = code - 1000*cy - 10*cx # digit 5 # für gewählte Schaltflächen
Welche Schaltfläche(n) gedrückt ist (sind), ergibt sich aus den if-Abfragen von cb & Zahl:
if cb & 1 == 1: # 1. Schaltfläche
if cb & 2 == 2: # 2. Schaltfläche
if cb & 4 == 4: # 3. Schaltfläche
Durch die UND-Verknüpfung & werden auch mehrere gedrückte Schaltflächen erkannt. Die dritte Schaltfläche (Wertigkeit 4) ist Platzhalter für die Umschaltung zwischen Fernsteuerung und autonomen Modus (noch nicht implementiert).
Wie oben beschrieben, instanziieren wir die StateMachine 0 mit der Zeile
sm0 = rp2.StateMachine(0, blink_1hz, freq=2000, set_base=Pin(14))
Dabei wird die blaue LED mit einem Vorwiderstand von ca. 200 Ohm an GPIO 14 (phys. Pin 19) angeschlossen. Mit
if cb & 1 == 1:
print("blink")
sm0.active(1)
else:
sm0.active(0)
wird die StateMachine bei cb & 1 == 1 (wegen der &-Verknüpfung auch bei 3, 5 und 7) aktiviert, anderenfalls deaktiviert.
Im nächsten Schritt möchte ich einen Buzzer als Sirene hinzufügen. Im ersten Versuch nehme ich einen aktiven Buzzer und schließe diesen an Pin 16 an. Nach vielen Irrwegen begnüge ich mich damit, dass auch der Buzzer im Sekundentakt tönt, also auch das PIO-Programm blink_1hz verwendet. Allerdings müssen wir einen zweiten Pin „anmelden“ und diesen im @asm_pio-decorator ergänzen:
asm_pio(set_init=(rp2.PIO.OUT_LOW, rp2.PIO.OUT_LOW)) # für zwei Pins (beide OUT und LOW) .
Dann instanziieren wir eine zweite StateMachine sm1 mit GPIO 16 (phys. Pin 21)
sm1 = rp2.StateMachine(1, blink_1hz, freq=2000, set_base=Pin(16))
denn die Sirene soll beim Drücken der zweiten Schaltfläche ein- bzw. ausgeschaltet werden.
if cb & 2 == 2: # True auch bei cb==4 oder cb==6
print("active Buzzer ebenfalls blink_1hz")
sm1.active(1)
else:
sm1.active(0)
Mein Versuch, eine zweite PIO-Funktion in Anlehnung an die erste Funktion blink_1hz zu programmieren, endete mit Fehlermeldungen, die ich nicht aufklären konnte. Also erst einmal zufrieden mit dem Erreichten sein und eine Pause einlegen.
Von anderen Mikrocontrollern kenne ich die Möglichkeit, bei Verwendung eines passiven Buzzers unterschiedliche Töne zu erzeugen. Vielleicht hat das schon jemand mit dem Pico und PIO probiert?
Also Suchmaschine anschmeißen und neben „Raspberry Pi Pico“ auch „PIO“ und „Töne“ verknüpfen. Bei Ben Everard, dem Chefredakteur der Zeitschrift „Hackspace“, werde ich fündig.
Er hat ein MicroPython-Modul mit Namen PIOBeep.py und ein Demo-Programm mit dem Lied Happy Birthday geschrieben. Hört sich schauderhaft an, aber man erkennt die Melodie. Ich möchte in Anlehnung an das Martin-Horn nur die Frequenzen 440Hz (Kammerton a) und 587 Hz (das darüberliegende d) verwenden, entscheide mich jedoch für die Tonfolge „tu-ta-ta-tu“, um Verwechslungen mit den Rettungsfahrzeugen auszuschließen. Hier zunächst der Code für das Modul, bei dem wir weitere Elemente der PIO-Assembly-Language kennenlernen werden.
Quelle: https://github.com/benevpi/pico_pio_buzz
from machine import Pin
from rp2 import PIO, StateMachine, asm_pio
from time import sleep
max_count = 5000
freq = 1000000
#based on the PWM example.
sideset_init=PIO.OUT_LOW) (
def square_prog():
label("restart")
pull(noblock) .side(0)
mov(x, osr)
mov(y, isr)
#start loop
#here, the pin is low, and it will count down y
#until y=x, then put the pin high and jump to the next section
label("uploop")
jmp(x_not_y, "skip_up")
nop() .side(1)
jmp("down")
label("skip_up")
jmp(y_dec, "uploop")
#mirror the above loop, but with the pin high to form the second
#half of the square wave
label("down")
mov(y, isr)
label("down_loop")
jmp(x_not_y, "skip_down")
nop() .side(0)
jmp("restart")
label("skip_down")
jmp(y_dec, "down_loop")
class PIOBeep:
def __init__(self, sm_id, pin):
self.square_sm = StateMachine(0, square_prog, freq=freq, sideset_base=Pin(pin))
#pre-load the isr with the value of max_count
self.square_sm.put(max_count)
self.square_sm.exec("pull()")
self.square_sm.exec("mov(isr, osr)")
#note - based on current values of max_count and freq
# this will be slightly out because of the initial mov instructions,
#but that should only have an effect at very high frequencies
def calc_pitch(self, hertz):
return int( -1 * (((1000000/hertz) -20000)/4))
def play_value(self, note_len, pause_len, val):
self.square_sm.active(1)
self.square_sm.put(val)
sleep(note_len)
self.square_sm.active(0)
sleep(pause_len)
def play_pitch(self, note_len, pause_len, pitch):
self.play_value(note_len, pause_len, self.calc_pitch(pitch))
Mit der Verlagerung des Programmcodes in ein Modul ist dieser leichter portierbar in andere Anwendungen. Die Grundidee ist Verwendung von Pulsweiten-Modulation (PWM) mit 50% Duty Cycle, also eine Rechteckspannung mit gleichen Ein- und Ausschaltzeiten.
Unterschiede zum vorherigen PIO-Assembly-Programm:
Mit dem Importieren der rp2-Modulteile durch
from rp2 import PIO, StateMachine, asm_pio
kann man beim Aufruf von PIO, StateMachine und asm_pio auf das vorangestellt rp2. verzichten.
Beim @asm_pio-decorator und in der folgenden PIO-Assembler-Funktion werden anstelle von set_init und set() für das Schalten der GPIO-Pins sideset_init und .side() verwendet.
Während set() einer der neun Befehle (s.o.) ist, wird die Anweisung .side() als „instruction modifier“ bezeichnet und an einen anderen Befehl angehängt.
Die “instruction modifiers” sind:
Noch ein Wort zu nop(): Diese Anweisung gehört nicht zu den neun Befehlen und wird als „Pseudo Instruction“ bezeichnet, die als mov(y, y) assembliert wird und nichts bewirkt.
In meinem MicroPython-Programm muss ich nun zusätzlich das Modul PIOBeep (das selbstverständlich auf dem Pico, ggf. im Unterverzeichnis lib, abgespeichert werden muss) importieren und weitere Zeilen aus dem Beispiel kopieren.
import PIOBeep
beeper = PIOBeep.PIOBeep(0,16)
# frequencies of the notes, standard pitch (Kammerton a) is notes[5]=440 Hz
notes = [261, 293, 330, 349, 392, 440, 494, 523, 587, 659, 698, 784, 880, 988, 1046]
notes_val = []
for note in notes:
notes_val.append(beeper.calc_pitch(note))
#the length the shortest note and the pause
note_len = 0.1
pause_len = 0.1
Für den Aufruf der Sirene definiere ich eine Funktion
def tutatatu():
global buzzTime
buzzTime = time.ticks_ms()
beeper.play_value(note_len*4, pause_len, notes_val[8])
beeper.play_value(note_len*2, pause_len, notes_val[5])
beeper.play_value(note_len*2, pause_len, notes_val[5])
beeper.play_value(note_len*4, pause_len, notes_val[8])
Das Einschalten der Sirene und der Wiederholungen nach 10 Sekunden erfolgt im Hauptteil des Programms
if cb & 2 == 2:
print("tutatatu")
ticksNow = time.ticks_ms()
print("ticksNow = ", ticksNow)
print("buzzTime = ", buzzTime)
if time.ticks_diff(ticksNow,buzzTime) > 10000:
tutatatu()
else:
pass
Da das Modul PIOBeep die StateMachine 0 benutzt, muss ich nun noch die StateMachine für die blaue LED ändern in
sm1 = rp2.StateMachine(1, blink_1hz, freq=2000, set_base=Pin(14))
Hier das komplette Programm für mein Robot Car mit Blaulicht und Sirene zum Download.
Viel Spaß beim Ausprobieren oder Anpassen an eigene Projekte.
]]>Mit der Fähigkeit E-Mails versenden zu können, haben wir aus dem ESP32 einen Packesel gemacht, der weltweit Post zustellen kann. In diesem Beitrag werden wir für die entsprechende Payload sorgen. Dazu schauen wir uns einen BME280 näher an, um dann ein Programm zu entwickeln, das uns die Daten des Sensors via E-Mail zustellt. Willkommen bei einer neuen Folge aus der Reihe
heute
Dass der ESP8266 für diesen Job nicht zu gebrauchen ist, liegt an dem knapp bemessenen Speicher. Unter MicroPython kann nur 1MB angesprochen werden und davon belegt bereits der Kernel einen Großteil. So kommt es, dass schon beim Importieren des Moduls BME280 ein Speicherproblem gemeldet wird. Das bedeutet, dass für diesen Beitrag ein ESP32 zwingend erforderlich ist.
Um den Zustand der Schaltung jederzeit auch direkt vor Ort einsehen zu können, habe ich dem ESP32 ein kleines Display spendiert, das über den I2C-Bus angesteuert wird. Es ist sogar grafikfähig und könnte daher auch zeitliche Änderungen des Messsignals als Kurve darstellen. Über die Flash-Taste ist ein geordneter Abbruch des Programms möglich. Das ist nützlich, falls zum Beispiel Aktoren sicher ausgeschaltet werden müssen, oder ein Abbruch über Strg+C erfolglos ist.
Abbildung 1: E-Mails vom ESP32
Als Messanwendung habe ich mich für einen Klimamonitor mit dem BME280 entschieden. Der Bosch-Sensor kann Luftdruck, relative Luftfeuchte und Temperatur erfassen. Mit diesen Daten werden wir den Luftdruck auf Meeresspiegelhöhe (NHN Normalhöhennull) und den Taupunkt berechnen.
1 |
ESP32 Dev Kit C unverlötet oder ESP32 Dev Kit C V4 unverlötet oder ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder NodeMCU-ESP-32S-Kit oder |
1 |
|
1 |
GY-BME280 Barometrischer Sensor für Temperatur, Luftfeuchtigkeit und Luftdruck |
1 |
|
diverse |
Jumper Wire Kabel 3 x 40 STK. je 20 cm M2M/ F2M / F2F evtl. auch |
optional |
Thonny oder
ssd1306.py Hardwaretreiber für das OLED-Display
oled.py API für das OLED-Display
bme280.py API für den Bosch-Sensor
bme280-test.py Demo- und Testprogramm für den BME280
umail.py Micro-Mail-Modul
e-mail.py Demoprogramm für den e-Mailversand
bme280-monitor.py Demo-Messprogramm
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 18.06.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Signale auf dem I2C-Bus
Immer wenn es Probleme bei der Datenübertragung gibt, setze ich gerne das DSO (Digitales Speicher Oszilloskop) ein, oder ein um Welten billigeres kleines Tool, einen Logic-Analyzer (LA) mit 8 Kanälen. Das Ding wird an den USB-Bus angeschlossen und zeigt mittels einer kostenlosen Software, was auf den Busleitungen los ist. Dort, wo es nicht auf die Form von Impulsen ankommt, sondern lediglich auf deren zeitliche Abfolge, ist ein LA Gold wert. Während das DSO nur Momentaufnahmen des Kurvenverlaufs liefert, kann man mit dem LA über längere Zeit abtasten und sich dann in die interessanten Stellen hineinzoomen. Eine Beschreibung zu dem Gerät finden Sie übrigens in dem Blogpost "Logic Analyzer -Teil 1: I2C-Signale sichtbar machen" von Bernd Albrecht. Dort ist auch beschrieben, wie man den I2C-Bus abtastet.
Abbildung 2: Logic Analyzer am I2C-Bus
Die drei Teile für die Schaltung sind schnell zusammengesteckt.
Abbildung 3: E-Mail - Schaltung
Auf dem BME280-Board befinden sich neben dem Sensor selbst noch ein Spannungswandler und ein Pegelwandler für die SCL und SDA-Leitung. Somit müssen keine externen Pullup-Widerstände angebracht werden, denn die sind Teil des Pegelwandlers. Die Spannungsversorgung erfolgt über die USB-Buchse aus dem PC, oder durch ein Steckernetzteil.
Während der BMP280 nur Temperatur und Luftdruck erfassen kann, kann sein großer Bruder auch die relative Luftfeuchte messen. Die Nummern und Bedeutung der Register des BMP280 sind beim BME280 gleich. Bei letzterem kommt eben nur der Bereich Feuchte dazu. Das ist praktisch, denn dadurch lässt sich das MicroPython-Modul BME280 auch für den BMP280 verwenden.
Beide Sensoren können über den I2C- oder SPI-Bus als Slave angesteuert werden. Allerdings ist beim vorliegenden Modul der I2C-Bus fest eingestellt, was mich nicht stört, weil auch das Display denselben Bus benutzt. Das I2C-Bus-Objekt wird daher auch im Hauptprogramm erzeugt und an die Konstruktoren OLED() und BME280() übergeben.
Daten können zum BME280 im Single-Byte-Mode oder im Multi-Byte-Mode, dann als Adress-Wert-Paare, gesendet werden. Beim Auslesen der Messwerte ist es praktisch, dass nur die Adresse des ersten Registers einer ganzen Folge angegeben werden muss und der BME280 die Adresse für weitere Lesezugriffe selbst erhöht (Autoinkrement). Im MicroPython-Modul bme280.py gibt es für den Datentransfer entsprechend zwei Methoden.
def writeByteToReg(self,reg,data):
buf=bytearray(2)
buf[0]=reg
buf[1]=data & 0xFF
self.i2c.writeto(self.hwadr,buf)
def readBytesFromReg(self,reg,num):
buf=bytearray(num)
buf[0]=reg
self.i2c.writeto(self.hwadr,buf[:1])
self.i2c.readfrom_into(self.hwadr,buf)
return buf
Beim Lesen gebe ich also nur das Startregister an und die Anzahl zu lesender Bytes. Damit wird ein Bytearray der gewünschten Länge erzeugt. Zum Senden der Adresse verwende ich nur das erste Element, in welches ich die Adresse schreibe. Dann werden so viele Bytes abgeholt, wie in das Array passen.
Das Datenblatt des BME280 gibt Aufschluss über die Registerlandschaft. In drei Registern werden die Konfigurationsdaten gehalten. Die Routinen zum Schreiben und Auslesen der Register greifen auf die Konfigurationsattribute des BME280-Objekts und auf die Routinen für den I2C-Transfer zurück.
Ein Statusregister informiert über den Systemzustand des Sensors. Nach jeder Messung wird der Satz an Rohdaten in Schattenregister geschrieben. Das Bit status.measuring ist 0, wenn die Daten zum Auslesen bereitstehen.
Im Read-Only-Register id = 0xD0 steht ein Indentifikationsbyte, das den Typ des Sensors verrät. Die Routine readIDReg() liefert die Klartextbezeichnung zurück.
0x55: "BMP180",
0x58: "BMP280",
0x60: "BME280"
Werksseitig ist jeder Sensor mit einem Satz an Kalibrierdaten versehen worden. Damit der BME280 korrekte Werte liefert, müssen diese aus den Registern 0xE1 bis 0xF0 ausgelesen werden. Das macht die Methode getCalibrationData() automatisch beim Instanziieren eines BME280-Objekts. Zusammen mit den Rohdaten für Temperatur, relative Feuchte und Luftdruck, die mit der Methode readDataRaw() ausgelesen werden, berechnet man dann nach den Formeln im Datenblatt die Endwerte. Das erledigen die Methoden calcTemperature(), calcPressureH() und calcHumidity(). Die Methode calcPressureNN() rechnet den Luftdruck auf Meeresniveau zurück. calcDewPoint() berechnet den Taupunkt, das ist die Temperatur, ab der der in der Luft befindliche unsichtbare Wasserdampf beginnt zu kondensieren und Nebeltröpfchen zu bilden.
Die Genauigkeit der Messungen lässt sich durch die Oversampling-Werte über die Control-Register in jeweils 5 Stufen einstellen, x1, x2, x4, x8 und x16. Dazu muss zuerst die Konfiguration gesetzt werden. Danach werden die Attribute in die Control-Register geschrieben. Das Oversampling der Feuchtemessung besitzt ein eigenes Register.
>>> b.setControl(OST=5)
>>> b.writeContrlReg()
>>> b.setControlH(OSH=3)
>>> b.writeControlHReg()
Die Startwerte, die der Konstruktor einstellt, sind über die Methoden showControl() und showConfig() abrufbar.
>>> b.showConfig()
Standby= 125 Filter= 16
(2, 16)
>>> b.showControl()
OST= 1 OSP= 3 Mode= 3 OSH= 2
(1, 3, 3, 2)
Dann wird es Zeit für ein erstes Testprogramm.
# bme280-test.py
from machine import Pin, SoftI2C
from bme280 import BME280
from time import sleep
import os,sys
i2c=SoftI2C(scl=Pin(22),sda=Pin(21),freq=100000)
try:
b=BME280(i2c)
except OSError as e:
print("Kein BME280 ansprechbar",e)
# b.getCalibrationData()
# b.printCalibrationData()
print("")
b.readDataRaw()
sleep(0.5)
print(b.calcTemperature(),"°C")
print(b.calcPressureH(),"hPa @ Standort")
print(b.calcPressureNN(450),"hPa @ Sea level")
print(b.calcHumidity(),"%RH")
print(b.calcDewPoint(),"°C")
Weil bei einem ESP8266 gleich beim Start dieses Magerprogramms eine Fehlermeldung aufploppt, muss die I2C-Schnittstelle nicht durch das Programm ermittelt werden, sondern ist direkt auf einen ESP32 eingestellt. Der ESP8266 ist speichermäßig einfach zu schmalbrüstig.
>>> %Run -c $EDITOR_CONTENT
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
MemoryError: memory allocation failed, allocating 360 bytes
Während der Entwicklung ist es nützlich, die Kalibrierdaten aus dem NVM (Non Volatile Memory = nicht flüchtiger Speicher) des BME280 zu kennen, um die berechneten Endwerte mit Hilfe der im Datenblatt angegebenen Formeln händisch zu überprüfen. b.getCalibrationData() und b.printCalibrationData() rufen die Werte ab und zeigen sie an.
Ich hatte oben schon erwähnt, dass die Messwerte in einen Schattenspeicher übertragen werden, sobald die Messung beendet ist. Um nach dem Start zuverlässige Werte zu erhalten, wird die erste Gruppe eingelesen und verworfen. Die nachfolgenden calcXY()-Aufrufe fordern dann jedes Mal einen neuen Satz an Rohdaten an. Damit ist sichergestellt, dass für die Druck- und Feuchteberechnung auch der korrekte Temperaturwert zur Verfügung steht. Ein Programmlauf liefert nun eine Ausgabe im Terminal, die ähnlich wie die folgende aussehen sollte.
17.78 °C
983.2547 hPa @ Standort
1037.413 hPa @ Sea level
42.30078 %RH
4.789577 °C
Es ist nicht schwierig, diese Daten per E-Mail zu versenden, wir müssen nur das Programm e-mail.py aus der letzten Folge mit bme280-test.py kombinieren. Das Endprodukt habe ich bme280-monitor.py genannt. Schauen wir uns an, wie es arbeitet.
import umail
import network
import sys
from time import sleep, ticks_ms
from machine import SoftI2C,Pin
from bme280 import BME280
from oled import OLED
Die beiden projektspezifischen Importe sind umail und bme280. Die entsprechenden Dateien müssen zum ESP32 hochgeladen werden, weil sie nicht Teil des MicroPython-Kernels sind, wie die anderen Beigaben.
Für den Zugriff auf das WLAN müssen Sie ihre eigenen Credentials eintragen.
# Geben Sie hier Ihre eigenen Zugangsdaten an
mySSID = 'EMPIRE_OF_ANTS'
myPass = 'nightingale'
Außerdem benötigen Sie Daten für den G-Mailzugang inklusive App-Passwort. Wie Sie zu beiden kommen, ist in der vorangehenden Folge erklärt. Vergessen Sie auch nicht bei recipient_email Ihre Mailadresse einzutragen.
# e-Mail-Daten
sender_email = 'ernohub@gmail.com'
sender_name = 'ESP32' #sender name
sender_app_password = 'xxxxxxxxxxxxxxxx'
recipient_email ='meine@mail.org'
Für das OLED-Display und den BME280 brauchen wir den I2C-Bus. Beide Bausteine unterstützen Geschwindigkeiten bis 400000kHz. Den ersten Datensatz vom BME280 verwerfen wir, das heißt, wir tun nix damit.
i2c=SoftI2C(scl=Pin(22),sda=Pin(21),freq=100000)
d=OLED(i2c,heightw=32)
d.writeAt("BME280-MAILER",0,0)
bme=BME280(i2c)
bme.readDataRaw()
Die Flashtaste ist die Taste für den geordneten Ausstieg aus der Hauptschleife.
taste=Pin(0,Pin.IN,Pin.PULL_UP)
intervall=3600 # Sendeintervall in Sekunden
Im Zwei-Stunden-Abstand wird nach einer Datenerfassung eine Mail versandt. Den Zeitraum können Sie natürlich Ihren Bedürfnissen entsprechend anpassen.
temp,location,seaLevel,relHum,taupunkt=0,0,0,0,0
Die Variablen für die Messwerte initialisieren wir mit 0. Die Methode dafür ist etwas ungewöhnlich. Wieso geht das? Wir nutzen dabei die Methode des Packens und Entpackens von Tupeln. Der MicroPython-Interpreter macht aus den fünf Nullen und dem Zuweisungsoperator "=" erst einmal ein Tupel. Diesen Vorgang nennt man Packen.
>>> x=0,0,0,0,0
>>> x
(0, 0, 0, 0, 0)
Der Inhalt des Tupels wird aber sofort wieder auf die fünf Variablen verteilt, also entpackt.
>>> a,b,c,d,e=(0, 0, 0, 0, 0)
>>> a; b; c; d; e
0
0
0
0
0
>>> a,b,c,d,e = 0,0,0,0,0
Packen und entpacken geschieht also in einer Zeile.
connectStatus = {
1000: "STAT_IDLE",
1001: "STAT_CONNECTING",
1010: "STAT_GOT_IP",
202: "STAT_WRONG_PASSWORD",
201: "NO AP FOUND",
5: "UNKNOWN"
}
Das Dictionary connectStatus hilft bei der Übersetzung der Status-Codes von der WLAN-Schnittstelle in Klartext.
Das Ergebnis der Abfrage der MAC-Adresse durch die Funktion nic.config('mac') ist als bytes-Objekt recht kryptisch.
>>> nic = network.WLAN(network.STA_IF)
>>> nic.active(True)
>>> nic.config('mac')
b'\xf0\x08\xd1\xd2\x1e\x94'
Die Funktion hexMac() macht daraus Klartext, den Sie im Router eintragen müssen, damit der MAC-Filter dem ESP32 Einlass gewährt.
>>> hexMac(b'\xf0\x08\xd1\xd2\x1e\x94')
'f0-8-d1-d2-1e-94'
def hexMac(byteMac):
"""
Die Funktion hexMAC nimmt die MAC-Adresse im Bytecode und
bildet daraus einen String fuer die Rueckgabe
"""
macString =""
for i in range(0,len(byteMac)): # Fuer alle Bytewerte
macString += hex(byteMac[i])[2:] # String ab 2 bis Ende
if i <len(byteMac)-1 : # Trennzeichen
macString +="-" # bis auf letztes Byte
return macString
Die Closure TimeOut() erzeugt beim Aufruf einen Software-Timer. Die zurückgegebene Funktion compare() weisen wir später dem Bezeichner senden zu. Dieser Name ist ein Alias für die Funktion compare(). Rufen wir senden() auf, dann erhalten wir als Ergebnis True oder False. Das sagt uns, ob der Timer schon abgelaufen ist oder nicht.
def TimeOut(t):
start=ticks_ms()
def compare():
return int(ticks_diff(ticks_ms(),start)) >= t
return compare
Die Funktion getWerte() liest die Werte des BME280 ein und baut daraus Strings, die an die Variablen temp, location, sealevel, relHum und taupunkt übergeben werden. Weil das aus einer Funktion heraus erfolgt, müssen die Variablen als global deklariert sein. Ohne das Schlüsselwort global wären diese Variablen für die Funktion lokal, die zugewiesenen Inhalte könnten außerhalb der Funktion nicht abgerufen werden.
Durch die Formatangabe {:0.2f} werden die Fließkommawerte, die von den calcXY()-Funktionen zurückkommen, auf zwei Stellen nach dem Komma ausgegeben. Diese Art der Formatierung ist die einfachste Möglichkeit, Strings und numerische Werte zu mischen.
def getWerte():
global temp,location,seaLevel,relHum,taupunkt
bme.readDataRaw()
sleep(0.5)
bme.readDataRaw()
temp="{:0.2f} *C".format(bme.calcTemperature())
location="{:0.2f} hPa @ Standort".\
format(bme.calcPressureH())
seaLevel="{:0.2f} hPa @ Sea level".\
format(bme.calcPressureNN(450))
relHum="{:0.2f} %RH".format(bme.calcHumidity())
taupunkt="{:0.2f} *C".format(bme.calcDewPoint())
Es folgt die Verbindungsaufnahme zum WLAN-Router. Damit das Accesspoint-Interface des ESP32 uns nicht in die Suppe spukt, wird es deaktiviert. Das ist vor allem beim ESP8266 wichtig, schadet aber auch beim ESP32 nicht.
# ************** Zum Router verbinden *******************
#
nic=network.WLAN(network.AP_IF)
nic.active(False)
nic = network.WLAN(network.STA_IF) # erzeugt WiFi-Objekt
nic.active(True) # nic einschalten
MAC = nic.config('mac') # binaere MAC-Adresse abrufen und
myMac=hexMac(MAC) # in Hexziffernfolge umwandeln
print("STATION MAC: \t"+myMac+"\n") # ausgeben
Dann erzeugen wir ein Station-Interface-Objekt und aktivieren es. Die ausgelesene MAC-Adresse übergeben wir zum Übersetzen an hexMac().
Nach einer kurzen Verschnaufpause bauen wir die Verbindung auf. SSID und Passwort werden übergeben und der Status wird abgefragt. Solange wie der ESP32 noch keine IP-Adresse vom DHCP-Server erhalten hat, werden im Display und im Terminalbereich von Thonny im Sekundenraster Punkte ausgegeben. Im Display geschieht das durch Slicing des Strings points. Um im Terminal auf einer Zeile zu bleiben, teilen wir der print-Anweisung mit, dass das Zeilenende-Zeichen "\n" durch nichts ' ' ersetzt werden soll.
Dann fragen wir den Status erneut ab und lassen uns die Verbindungsdaten anzeigen.
print("\nStatus: ",connectStatus[nic.status()])
d.clearAll()
STAconf = nic.ifconfig()
print("STA-IP:\t\t",STAconf[0],"\nSTA-NETMASK:\t",\
STAconf[1], "\nSTA-GATEWAY:\t",STAconf[2] ,sep='')
print()
d.writeAt(STAconf[0],0,0)
d.writeAt(STAconf[1],0,1)
d.writeAt(STAconf[2],0,2)
sleep(3)
Vor dem Eintritt in die Hauptschleife, stellen wir den Timer auf die Zeit in intervall. Weil der Timer in Millisekunden tickt, wird der Wert mit 1000 multipliziert. Damit die erste Mail sofort verschickt wird, setzen wir jetzt auf True. Diese Variable hat aber noch eine zweite Bedeutung. Tritt nämlich ein Ereignis ein, das eine sofortige Versendung der Mail erforderlich macht, dann kann der auslösende Vorgang jetzt auf True setzen und so den Mailversand auch außerhalb der festen Zeitspanne veranlassen.
senden=TimeOut(intervall*1000)
jetzt=True
kontrolle=TimeOut(1800000)
oldPres= oldPres=float(seaLevel.split(" ")[0])
Dann holen wir die aktuellen Werte. Wir merken uns schon mal den Luftdruck in oldPres. Und stellen einen weiteren Timer auf eine halbe Stunde. In der Hauptschleife prüfen wir, ob der Timer kontrolle() schon abgelaufen ist. In diesem Fall merken wir uns den aktuellen Luftdruckwert und stellen den Timer neu.
while 1:
if kontrolle():
oldPres=float(seaLevel.split(" ")[0])
kontrolle=TimeOut(1800000)
Die neuen Werte werden eingelesen und die Änderung des Luftdrucks überprüft. Fällt der Luftdruck in der Kontrollzeitspanne um mehr als zwei hPa, dann könnte ein Gewitter im Anzug sein und eine Wetterwarnung wird vorbereitet Der Betreff wird geändert und jetzt wird True. Für den Vergleich der Zahlenwerte müssen diese aus dem String extrahiert werden. Dazu lasse ich den String an den Leerstellen " " aufteilen. Aus der erhaltenen Liste nehme ich das erste Element und wandle es in eine Fließkommazahl um.
>>> seaLevel
'1037.62 hPa @ Sea level'
>>> seaLevel.split(" ")
['1037.62', 'hPa', '@', 'Sea', 'level']
>>> seaLevel.split(" ")[0]
'1037.62'
>>> float(seaLevel.split(" ")[0])
1037.62
getWerte()
if oldPres - float(seaLevel.split(" ")[0]) > 2:
email_subject ='Unwetter im Anzug'
jetzt=True
d.clearAll(False)
d.writeAt(temp,0,0,False)
d.writeAt(seaLevel,0,1,False)
d.writeAt(relHum,0,2)
Die Ausgabe der Werte im Terminal und im Display ist nicht spektakulär. False in den writeAt-Befehlen zum Display verhindert das flackern der Anzeige. Es bewirkt, dass die Änderungen zuerst nur im Hintergrund im Puffer erfolgen. Erst mit dem letzten writeAt-Befehl wird der Pufferinhalt zum OLED-Display gesendet.
Eine E-Mail wird versandt, falls der Intervall-Timer abgelaufen ist, oder wenn jetzt den Wert True hat.
if senden() or jetzt:
# ************ Eine Mail versenden ***************
#
print(temp)
print(location)
print(seaLevel)
print(relHum)
smtp = umail.SMTP('smtp.gmail.com', 465,
ssl=True, debug=True)
smtp.login(sender_email, sender_app_password)
smtp.to(recipient_email)
smtp.write("From:" + sender_name + "<"+ \
sender_email+">\n")
smtp.write("Subject:" + email_subject + "\n")
smtp.write("Klimawerte vom ESP32\n")
smtp.write(temp+"\n")
smtp.write(location+"\n")
smtp.write(seaLevel+"\n")
smtp.write(relHum+"\n")
smtp.write(taupunkt+"\n")
smtp.send()
smtp.quit()
if email_subject == 'Unwetter im Anzug'
senden=TimeOut(intervall*1000)
jetzt=False
email_subject ='Wettermeldung'
Wir bauen eine Verbindung zum Provider auf und senden Username und App-Passwort, danach die Empfängeradresse, den Absender und den Betreff. Nach der Übertragung der Messdaten veranlassen wir die Versendung an den Empfänger, also uns selbst und beenden die Verbindung.
Es folgen Aufräumarbeiten. Der Intervall-Timer wird neu gestellt, wenn er abgelaufen war und der Alarmgeber wird zurückgesetzt, sowie der Betreff auf Normalbetrieb gebracht.
Die Abfrage des Zustands der Flash-Taste schließt das Programm ab.
if taste.value() == 0:
sys.exit()
Die Versendung einer E-Mail "on request", hier die Gewitterwarnung, kann natürlich auch durch verschiedene andere Ereignisse ausgelöst werden. Alles, was sich durch einen Sensor erfassen lässt, kann einen Mailversand triggern. Die Hauptschleife läuft im Dauerlauf und kann jederzeit weitere Sensoren abfragen. Personen, die sich in einem Raum bewegen, der Wasserstand im Regenfass, Sturm, Licht an oder aus sind nur ein paar Möglichkeiten. Lassen Sie Ihrer Phantasie freien Lauf. Das komplette Programm liegt natürlich zum Download bereit.
Viel Vergnügen beim Basteln und Programmieren!
]]>Um aus der Pampa mit dem ESP32/ESP8266 Nachrichten als SMS versenden zu können, braucht es ein GSM-Modul. Es steht dort ja selten ein LAN zur Verfügung. Darüber, dass es von daheim aus mit einem WLAN-Zugang der ESPs auf vielfältige Weise über TCP/HTTP, MQTT oder UDP auch klappt, habe ich auch schon geschrieben. Die Links sind nur eine kleine Auswahl. Mit TCP wird eine gesicherte bidirektionale Verbindung aufgebaut. So kann man auch von auswärts auf einen Webserver auf den ESPs zugreifen, wenn auf dem DSL-Router eine Portweiterleitung eingerichtet ist. Allerdings braucht es bei HTTP eine Anfrage vom Browser eines Clients, um die Verbindung aktiv herzustellen. Mit MQTT brauche ich einen Broker auf einem Heimserver, zum Beispiel einem Raspi, der den Datenaustausch koordiniert, Informationen sammelt und weitergibt.
In manchen Fällen ist es aber besser, wenn sich der ESP32 von sich aus beim Client, also einem Handy oder Tablet meldet, um eine Nachricht zu hinterlassen. Deren Eingang kann mit einem Signal verbunden werden. Die Rede ist von E-Mails. Ja und richtig, der ESP32 und der ESP8266 können beide E-Mail-Nachrichten aus einem LAN heraus versenden. Wie das geht, das erfahren Sie in dieser neuen Folge aus der Reihe
heute
Gleich vorneweg: E-Mails versenden können beide, der ESP32 wie auch der ESP8266, denn beide verfügen über ein WLAN-Interface. Das Testprogramm e-mail.py läuft ohne Änderung auf beiden Systemen. Das erforderliche Modul micro-Mail in umail.py ist mit seinen 126 Programmzeilen recht überschaubar.
Anders sieht es mit dem Modul bme280 aus. Die Anwendung bme280-monitor.py (folgt in Teil 2), die auf das Modul zurückgreift, ist aus Platzgründen nur auf einem ESP32 lauffähig.
Als Anwendung bietet sich aber jedes Ereignis an, das sich mittels Sensoren erfassen und vom Controller auswerten lässt – Land unter im Keller, Mausefalle hat zugeschnappt, Einbrecher in der Wohnung, und so weiter. Was wir also für dieses Projekt in jedem Fall brauchen, ist ein Controller. Ob es ein ESP8266 sein kann, oder ein ESP32 sein muss, das hängt letztlich von der Art und Vielfalt der weiteren Komponenten ab. Für jeden Wasserstandssensor reicht ein GPIO-Pin. Das gilt auch für einen akustischen Alarmgeber. Für ein Display brauche ich zwei bis drei, je nachdem, ob die Anzeige am I2C- oder am SPI-Bus hängt. Beim ESP8266 sind die digitalen Eingänge mit 9 Stück nicht gerade im Überfluss vorhanden und darüber hinaus durch andere Funktionen, meist den Start des Systems betreffend, in der Einsetzbarkeit stark eingeschränkt. Das zeigt die Tabelle 1.
Label |
GPIO |
Input |
Output |
Notes |
D0 |
GPIO16 |
Kein IRQ |
Kein PWM or I2C support |
HIGH at boot |
D1 |
GPIO5 |
OK |
OK |
SCL bei I2C-Nutzung |
D2 |
GPIO4 |
OK |
OK |
SDA bei I2C-Nutzung |
D3 |
GPIO0 |
pulled up |
OK |
FLASH button, wenn LOW Normales Booten, wenn HIGH |
D4 |
GPIO2 |
pulled up |
OK |
Muss beim Booten HIGH sein |
D5 |
GPIO14 |
OK |
OK |
SPI (SCLK) |
D6 |
GPIO12 |
OK |
OK |
SPI (MISO) |
D7 |
GPIO13 |
OK |
OK |
SPI (MOSI) |
D8 |
GPIO15 |
pulled to GND |
OK |
SPI (CS) |
RX |
GPIO3 |
OK |
RX pin |
Muss beim Booten HIGH sein |
TX |
GPIO1 |
TX pin |
OK |
Muss beim Booten HIGH sein |
A0 |
ADC0 |
Analog Input |
X |
Tabelle 1: Pinbelegung und Systemfunktionen beim ESP8266
Das gilt es bei der Auswahl des Controllers zu beachten. Der ESP32 ist dagegen in dieser Hinsicht wesentlich anspruchsloser.
Allein zum Versenden von E-Mails ist im Prinzip jeder der angeführten Typen einsetzbar. Die Auflistung der Bauteile enthält neben dem Controller auch bereits die Teile, die in der nächsten Folge zum Einsatz kommen werden.
Um den Zustand der Schaltung jederzeit auch direkt vor Ort einsehen zu können, habe ich dem ESP ein kleines Display spendiert, das über den I2C-Bus angesteuert wird. Es ist sogar grafikfähig und könnte daher auch zeitliche Änderungen des Messsignals als Kurve darstellen. Über die Flash-Taste wäre eine Umschaltung zwischen Text- und Grafikmodus machbar und bei längerem Drücken ein geordneter Abbruch des Programms, falls zum Beispiel Aktoren sicher ausgeschaltet werden müssen.
Als Messanwendung habe ich mich für einen Klimamonitor mit dem BME280 entschieden. Der Bosch-Sensor kann Luftdruck, relative Luftfeuchte und Temperatur erfassen. Der Chef in meiner Schaltung wird daher ein ESP32 sein.
1 |
D1 Mini NodeMcu mit ESP8266-12F WLAN Modul oder D1 Mini V3 NodeMCU mit ESP8266-12F oder NodeMCU Lua Amica Modul V2 ESP8266 ESP-12F WIFI oder NodeMCU Lua Lolin V3 Module ESP8266 ESP-12F WIFI oder ESP32 Dev Kit C unverlötet oder ESP32 Dev Kit C V4 unverlötet oder ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder NodeMCU-ESP-32S-Kit oder |
1 |
|
1 |
GY-BME280 Barometrischer Sensor für Temperatur, Luftfeuchtigkeit und Luftdruck |
1 |
|
diverse |
Jumper Wire Kabel 3 x 40 STK. je 20 cm M2M/ F2M / F2F evtl. auch |
optional |
Thonny oder
ssd1306.py Hardwaretreiber für das OLED-Display
oled.py API für das OLED-Display
umail.py Micro-Mail-Modul
e_mail.py Demoprogramm für den E-Mailversand
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 18.06.2022) auf den ESP-Chip gebrannt wird.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter boot.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Der Mikrocontroller hat keinen eigenen E-Mail-Server zum Empfangen und Senden von E-Mails. Man kann das über externe Provider realisieren. Es ist aus verschiedenen Gründen keine gute Idee, vom eigenen E-Mail-Konto aus Nachrichten durch einen ESP versenden zu lassen. Dagegen sprechen eindeutig Sicherheitsaspekte und auch eine mögliche Sperrung des Kontos, wenn der ESP Mist baut und zum Beispiel sehr viele Mails innerhalb kurzer Zeit versendet, oder andere Fehler verursacht, die der Provider ahndet. Deshalb beginne ich hier mit der Einrichtung eines neuen Google-Kontos, über das der ESP dann arbeiten kann. Als Empfänger können Sie ruhig Ihr normales Mail-Konto benutzen, beim gleichen Provider oder einem anderen, das ist egal.
Mit einem Handy, Tablet oder PC kann/muss man sich heute mit 2-Wege-Authentifizierung bei einem E-Mail-Provider anmelden, das ist der sichere Weg. Beim Anmelden anderer Geräte, wie zum Beispiel einem Mikrocontroller, funktioniert das nicht. Sie können den Authentifizierungslink oder die SMS ja nicht empfangen und bestätigen. Es ist schon einige Zeit her, als in solchen Fällen nur der Ausweg über die unsichere einfache Authentifizierung über Benutzername und Passwort möglich war. Das wurde von einigen Providern aber nicht mehr unterstützt.
Ich stelle Ihnen heute eine Möglichkeit vor, mit Ihrem ESP trotzdem eine Mail zu versenden. Das gelingt mit Hilfe eines App-Passworts, einer Kombination von 16 Zeichen, die Sie einer bestimmten Anwendung oder einem Gerät zuordnen können.
Beginnen wir mit der Erstellung eines neuen Google-Kontos. Folgen Sie dem Link und klicken Sie auf Konto erstellen.
Folgen Sie jetzt der Benutzerführung. Kleiner Tipp: es ist nicht nötig, Ihre tatsächlichen persönlichen Daten wie Name und Geburtsdatum anzugeben, Sie können sie auch faken. Nur müssen Sie spätestens im nächsten Kapitel über die Telefon- oder Handynummer erreichbar sein, um Ihre E-Mailadresse zu bestätigen.
Abbildung 1: Name und Passwort erfassen
Im ersten Schritt geben Sie einen Namen an, Google generiert daraus einige Mailadressen. Suchen Sie eine aus, oder geben Sie eine ein, aber bitte nicht Ihre Haupt-Mailadresse! Bauen Sie ein Passwort. - Weiter
Abbildung 2: Geburtsdatum und Geschlecht
Das Geburtsdatum kann wie der Name gefaket werden. Wählen Sie am besten eins aus den Jahren vor 2004. – Weiter
Abbildung 3: Personalisierungseinstellungen
Damit Sie erfahren, was alles zu Personalisierung gehört, wählen Sie Manuelle Personalisierung, auch wenn das länger dauert.
Abbildung 4: Aktivitäten nicht speichern
Sie werden ihr Mailkonto selten abrufen, da ist es Ihnen sicher egal, was so alles gespeichert werden kann. – Weiter
Abbildung 5: YouTube-Verlauf nicht speichern
Auch der YouTube-Verlauf kann Ihnen wurscht sein, auch was an Werbung reinflattert. - Weiter - Weiter
Abbildung 6: Allgemeine Werbung
Abbildung 7: Keine Erinnerungen
Weil Sie ja vermutlich nix an den Einstellungen ändern wollen, brauchen Sie auch keine Erinnerungen. - Weiter - und dann war's das auch schon.
Abbildung 8: Zusammenfassung
Ganz unten finden Sie einen Weiter-Button und dann kommen Sie mit Linksklick auf GoogleKonto links oben zur Startseite ihres neuen Accounts. Melden Sie sich jetzt ab.
Nach der Neuanmeldung bekommen Sie wahrscheinlich eine Meldung eingeblendet.
Abbildung 9: Intelligente Funktionen deaktivieren
Die intelligenten Funktionen können Sie deaktivieren, weil der ESP32/ESP8266 die eh nicht nutzen wird.
Auf der Startseite rechts oben klicken Sie auf den Button mit Ihrem Initial. Mit Linksklick auf Google-Konto verwalten wird links ein Menü eingeblendet. Klicken Sie dort auf Sicherheit.
Abbildung 10: Sicherheit aufrufen
Abbildung 11: Bei Google anmelden
Im Fenster Sicherheit scrollen Sie bis Bei Google anmelden. Bevor ein App-Passwort vergeben werden kann, muss Bestätigung in zwei Schritten aktiviert werden. Starten Sie mit Klick auf das Größer-Zeichen.
Abbildung 12: Smartphone einrichten
Jetzt brauchen Sie die Handynummer. Hier müssen Sie leider Ihre eigene angeben, weil Google Ihnen einen Bestätigungscode schickt.
Abbildung 13: Nummer bestätigen
Per SMS erhalten Sie einen sechsstelligen Bestätigungscode. - Weiter - Aktivieren
Abbildung 14: Aktivieren
Abbildung 15: Bestätigung aktiviert
Mit Klick auf GoogleKonto kommen Sie zurück zur Verwaltungsseite. Scrollen Sie erneut in Sicherheit bis Bei Google anmelden.
Abbildung 16: Sicherheit - App-Passwörter
Es ist noch kein App-Passwort eingerichtet, also erzeugen wir ein erstes. Sie können auch mehrere erstellen, für jeden Client ein eigenes. Klick aufs Größer-Zeichen.
Abbildung 17: Erneute Authentifizierung
Google fordert eine erneute Authentifizierung mit Username und Passwort. - Weiter
Abbildung 18: App auswählen
Als App wählen Sie E-Mail aus, als Gerät Andere.
Abbildung 19: Gerät auswählen
Dann geben Sie einen beliebigen Gerätenamen an. - Generieren
Abbildung 20: Gerätenamen angeben
Abbildung 21: Generiertes App-Passwort merken
Auch wenn es bei Verwendung anders steht, merken Sie sich in jedem Fall das erzeugte Passwort, eine Folge von 16 Kleinbuchstaben. Sie brauchen diese später für das Programm. Bewahren Sie den Code sicher auf. - Fertig
Abbildung 22: Das app-Passwort ist gespeichert
Damit ist das App-Passwort erzeugt und Sie können loslegen.
Abbildung 23: Jetzt können Sie loslegen
Das SMTP-Protokoll (Simple Mail Transfer Protocol), das zum Versand von E-Mails eingesetzt wird, ist ein menschenlesbares Protokoll, das auf TCP-Datenströmen aufsetzt. Für die Übertragung wird also zwischen Client und Server eine gesicherte, bidirektionale Verbindung ausgehandelt. "gesichert" bezieht sich hier auf die Eigenschaften verlustfrei und fehlerkorrigierend, nicht auf abhörsicher. Gleichwohl wird eine Art Verschlüsselung zum Transfer von Username und Passwort angewandt, Base64. Bei diesem Coding werden aus drei normalen Bytes vier ASCII-Zeichen aus dem Bereich A-Z, a-z, 0-9,+ und /. Anhänge, wie zum Beispiel Bilder, werden auch in Base64 codiert. Das erklärt, warum das Volumen bei der Bildübertragung zunimmt.
Laden Sie sich jetzt als Erstes die MicroPython-Datei umail.py herunter. Speichern Sie sie in Ihrem Arbeitsverzeichnis (_workspace von Thonny). Laden Sie die Datei auf den ESP32/ESP8266 hoch. Das Original stammt von Shawwwn, für Debugging-Zwecke habe ich einige print-Zeilen eingebaut, die auch die Struktur einer Übertragung offenbaren. Die Ausgabe im Terminal erfolgt, wenn Sie beim Konstruktoraufruf den Parameter debug = True setzen.
smtp = umail.SMTP('smtp.gmail.com', 465, ssl=True, debug=True)
Aber gehen wir der Reihe nach vor.
import umail
import network
import sys
from time import sleep
from machine import SoftI2C,Pin
# Geben Sie hier Ihre eigenen Zugangsdaten an
mySSID = 'EMPIRE_OF_ANTS'
myPass = 'nightingale'
Einige Importe versorgen uns mit den nötigen Zutaten, umail ist hier die wichtigste. Für mySSID und myPass geben Sie bitte die Credentials für Ihren WLAN-Router an.
class MailError(Exception):
pass
class UnkownPortError(MailError):
def __init__(self):
print("System-Fehler\nunbekannter Port")
print("Nur ESP32, ESP8266 werden unterstützt")
Die beiden Exception-Klassen dienen der Fehlerbehandlung bei der Bestimmung des Ports.
if sys.platform == "esp8266":
i2c=SoftI2C(scl=Pin(5),sda=Pin(4))
elif sys.platform == "esp32":
i2c=SoftI2C(scl=Pin(22),sda=Pin(21),freq=100000)
else:
raise UnkownPortError()
Eine UnkownPortError-Exception wird geworfen, wenn sich der Controller weder als ESP32 noch als ESP8266 identifiziert.
sender_email = 'ernohub@gmail.com'
sender_name = 'ESP32' #sender name
sender_app_password = 'xxxxxxxxxxxxxxxx'
='meine@mail.org'
email_subject ='Test e-Mail'
sender_email ist der Google-Account mit dem zugehörigen App-Passwort, das wir oben erzeugt haben. Bei recipient_email geben Sie Ihre Mailadresse an, unter der Sie die Meldungen empfangen möchten.
connectStatus = {
1000: "STAT_IDLE",
1001: "STAT_CONNECTING",
1010: "STAT_GOT_IP",
202: "STAT_WRONG_PASSWORD",
201: "NO AP FOUND",
5: "UNKNOWN"
}
Das Dict connectStatus übersetzt die Statuscodes des Station-Interfaces in Klartext.
def hexMac(byteMac):
"""
Die Funktion hexMAC nimmt die MAC-Adresse im Bytecode und
bildet daraus einen String fuer die Rueckgabe
"""
macString =""
for i in range(0,len(byteMac)): # Fuer alle Bytewerte
macString += hex(byteMac[i])[2:] # String ab 2 bis Ende
if i <len(byteMac)-1 : # Trennzeichen
macString +="-" # bis auf letztes Byte
return macString
Die Funktoin hexMac() verrät uns die MAC-Adresse der WLAN-Schnittstelle.
Abbildung 23: MAC-Abfrage
Diese muss im Router eingetragen werden, damit der Controller vom Türsteher (MAC-Filter) Einlass erhält. Bei einem TP-LINK-Router sieht das so aus:
Abbildung 24: MAC-Filtering
Abbildung 25: MAC-Filtering - Eintrag
Dann bauen wir eine Verbindung zum Router auf. Während der Vorgang läuft, werden im Sekundenabstand Punkte im Terminal ausgegeben.
Eine Mail zu versenden ist Dank des Moduls umail kein großer Akt.
# ************** Eine Mail versenden *******************
#
smtp = umail.SMTP('smtp.gmail.com', 465, ssl=True, debug=True)
smtp.login(sender_email, sender_app_password)
smtp.to(recipient_email)
smtp.write("From:" + sender_name + "<"+ sender_email+">\n")
smtp.write("Subject:" + email_subject + "\n")
smtp.write("Greetings from ESP32/ESP8266")
smtp.send()
smtp.quit()
Der Konstruktoraufruf erzeugt ein SMTP-Objekt. Wir übergeben die URL des Servers und die Portnummer des SMTP-Portals. Dann senden wir unsere G-Mail-Kennung und das App-Passwort. Der Empfänger wird übertragen, danach der Name und die Mailadresse des Absenders. Es folgt ein Betreff und dann senden wir den Text der Nachricht. Längere Texte können in mehrere write-Anweisungen aufgeteilt werden. Bisher haben wir die Daten nur an den G-Mail-Server übertragen, der sie fleißig aufgesaugt hat. Mit send geben wir den Auftrag für die Weiterleitung an uns als Empfänger, bevor wir die Verbindung zum Server kappen.
Abbildung 26: esp32 sendet E-Mails vom bme280
In der nächsten Blogfolge werden wir den BME als Messwertaufnehmer für Druck, relative Feuchte und Temperatur in Dienst stellen. Im Programm können wir eine zeitliche Steuerung vorsehen, sowie eine Alarmfunktion, die sofort eine Mail sendet, wenn zum Beispiel der Luftdruck in kurzer Zeit stark fällt, was bei einem anziehenden Gewitter der Fall ist.
Bis dann!
]]>In diesem Blogbeitrag konzentrieren wir uns darauf, die Daten zu verwenden, um die Beleuchtung eines Aquariums im Demohaus automatisiert zu steuern. Zum Einsatz kommt hier eine Regel, die ein sogenanntes Blockly-Skript ausführt.
Für diesen Teil brauchen Sie eine fertig eingerichtete Installation von openHAB. Zusätzlich nutzt dieser Beitrag eine mit Tasmota geflashte Steckdose, welche mit einem MQTT-Broker kommuniziert. Dafür sollte der MQTT-Broker aus Blogbeitrag 2 installiert, eingerichtet und als Binding in openHAB eingerichtet sein. Das Binding für Astro werden Sie in diesem Beitrag installieren und einrichten.
Gleich vorab muss hier gesagt sein, dass sich das Hinzufügen eines Thing, also als Komponente, die später gesteuert werden soll, von Hersteller zu Hersteller unterscheidet. Viele Geräte können MQTT, aber auch hier unterscheidet sich die Art, wie das Device Daten empfängt und sendet. Daher ist auf die Dokumentation des Herstellers des Devices zu achten, bzw. sollte ein guter MQTT-Viewer genutzt werden. In diesem Beispiel wird ein NOUS-Device mit Tasmota-Firmware verwendet.
In diesem Blog verwende ich den Skripteditor Blockly. Dieser ist sehr mächtig, hält aber viele Funktionen recht einfach. Eine komplette Übersicht der Kommandos und Funktionen kann in diesem Beitrag nicht gegeben werden, das würde einfach den Rahmen sprengen. Hierzu gibt es verschiedene (Video-)Tutorials im Internet zu finden, die Teile von Blockly sehr gut erklären.
Im ersten Beitrag dieser Serie habe ich empfohlen, das Binding Astro zu installieren. Sollte das noch nicht geschehen sein, so sollten Sie dies nachträglich installieren. Gehen Sie über Einstellungen -> Bindings, siehe Abbildung 1.
Abbildung 1: Binding installieren
Dort suchen Sie unten rechts nach Astro und installieren es direkt im Anschluss, siehe Abbildung 2.
Abbildung 2: Das Astro Binding installieren
Ist das Binding installiert, wechseln Sie zu Einstellungen -> Things und wählen, nachdem Sie das „+“ in der rechten unteren Ecke gedrückt haben, den Punkt Astro Binding aus, siehe Abbildung 3.
Abbildung 3: Astro Binding auswählen
Im darauffolgenden Dialog wählen Sie den Eintrag Lokale Sonnendaten und bestätigen mit OK, siehe Abbildung 4.
Abbildung 4: Lokale Sonnendaten hinzufügen
Im gleichen Zug müssen noch wichtige Actions installiert werden, damit später die Blockly-Programmierung funktioniert.
Suchen Sie einmal nach:
Haben Sie diese über die Suche gefunden, installieren Sie diese. Mehr zu diesen Modulen wird bei der Programmierung via Blockly erklärt.
Als nächstes erstellen Sie das Thing für die Steckdose, die später dann das Licht des Aquariums steuern soll. Im Bereich Things fügen Sie, über das „+“ am rechten unteren Rand, ein neues Thing hinzu und wählen MQTT Binding, siehe Abbildung 5.
Abbildung 5: MQTT Binding hinzufügen
Im darauffolgenden Dialog wählen Sie Generic MQTT Thing und vergeben eine Location, ein Label und geben die Parent Bridge an. Danach erstellen Sie das MQTT Thing, siehe Abbildung 6.
Abbildung 6: MQTT Thing anlegen
Bis zu diesem Punkt unterscheidet sich der Vorgang nicht zu dem aus dem letzten Beitrag, bei dem ein Sensor als MQTT-Thing angelegt wurde. Nun legen Sie, wie schon im vorherigen Beitrag, einen Channel über Channel -> Add Chanel im entsprechenden MQTT Thing an. Vergeben Sie einen Namen und ein Label für das Thing. Wichtig an der Stelle ist, dass als Channel type ON/OFF Switch gewählt wird, siehe Abbildung 7.
Abbildung 7: Definition des Channels
Nun kommt der knifflige Teil, der sich von Hersteller zu Hersteller unterscheidet, die Konfiguration der Topics. Hier gibt es zwei Topics, da es einen Command-Topic und einen Status-Topic gibt, siehe Abbildung 8.
Abbildung 8: Die Topics korrekt festlegen
Bei MQTT State wird der aktuelle Status des Devices wiedergegeben, mit MQTT Command übergeben Sie einen neuen Wert. Über die letzten beiden Felder können Sie ein anderes Statuswort zum ein- bzw. ausschalten des Devices festlegen. In der Regel müssen Sie hier aber nichts eintragen. Im Anschluss drücken Sie auf Create und der Channel sollte angelegt sein.
Zuletzt soll nun noch die Steckdose in unserem Model auftauchen und korrekt einem Aquarium im Wohnzimmer zugeordnet werden. Wechseln Sie dazu ins Model und klappen Sie alles so weit auf, bis Sie am Wohnzimmer angekommen sind. Wählen Sie das Wohnzimmer aus und drücken Sie Create Equipment from Thing, siehe Abbildung 9.
Abbildung 9: Equipment zum Modell hinzufügen
Im neuen Dialog drücken Sie auf Thing und wählen das zuvor erzeugte MQTT-Device aus, siehe Abbildung 10. Wichtig an der Stelle, Sie sollten eindeutige Namen vergeben, damit Sie später schnell und einfach das Device finden.
Abbildung 10: Ein Thing dem Equipment zuweisen
Übergeben Sie anschließend die richtige Category des Equipments und wählen unten die korrekte Category, Type und Semantic Class des Things aus, siehe Abbildung 11.
Abbildung 11: Abschließende Konfiguration
Hat alles soweit geklappt, sollte das Equipment und ein weiteres Item in dem Model erscheinen. Interessant dabei ist, dass Sie einen Switch in der Oberfläche haben, mit dem Sie direkt die Steckdose für das Licht ein- bzw. ausschalten können, siehe Abbildung 12.
Abbildung 12: Equipment und Item im Model
Es bietet sich an der Stelle an, diesen Schalter einmal auszuprobieren, damit später beim Erstellen des Skripts ausgeschlossen werden kann, dass die Kommandos und States in MQTT nicht falsch eingestellt wurden.
Bis zum jetzigen Zeitpunkt hat unsere Steuerung noch keinerlei Automation. Es können Daten via MQTT empfangen werden und eine Steckdose ist mit MQTT verbunden, die auf Kommando ein oder ausgeschaltet werden kann. Das hilft in unserem jetzigen Beispiel aber nicht, eine Lampe eines Aquariums automatisch ein- und auszuschalten. Möglich macht diese ein Automatismus mit definierten Regeln, die Sie im Bereich Rules erstellen können, siehe Abbildung 13.
Abbildung 13: Der Bereich Rules für Automatisierung
Interessant finde ich dabei direkt den Text, der diesen Bereich super beschreibt: „Regeln sind die Grundbausteine für die Automatisierung des Hauses - du definierst, welche Aktionen ausgeführt werden sollen, wenn bestimmte Ereignisse eintreten.“. Sprich es ist genau das, was in diesem Beispiel das Aquarium braucht. Über das „+“ unten rechts legen Sie eine neue Regel an. Anfangen sollten Sie dann wie immer, zunächst mit einem aussagekräftigen Namen und einer kurzen Beschreibung, siehe Abbildung 14.
Abbildung 14: Basisinformationen zu der Regel
Weiter unten wird nun eingestellt, was genau in dieser Regel wann passieren soll.
Alle drei Ansichten sehen beinah identisch aus, als Beispiel siehe Abbildung 15, bei dem man zwischen einem Item, Trigger, Time oder System Event auswählen kann.
Abbildung 15: Dialog von WHEN
Bei dem Dialog von THEN wird das Wort Event durch Action und bei BUT ONLY IF durch Condition ersetzt. Großartig ist, dass die Dialoge identisch aussehen und man sich schnell zurechtfindet.
Kommen wir nun auf das Beispiel mit der Lampe im Aquarium zurück. Ich will es an der Stelle einfach halten, damit der Einstieg leichtfällt. Die Lampe des Aquariums soll an gehen, wenn die Zeit des Sonnenaufgangs in meiner Region erreicht ist und ebenso soll die Lampe aus gehen, wenn die Zeit des Sonnenuntergangs in meiner Region erreicht ist. Einige Sonderfälle blende ich an der Stelle bewusst aus. Ich möchte allerdings ein bisschen Sicherheit mit einbauen, damit wenn das MQTT-Device ausgesteckt und wieder eingesteckt wurde, die Lampe dennoch wieder korrekt geschaltet wird. Anbieten würde sich, dass zyklisch alle 30 Sekunden die Bedingung für Sonnenauf- und -untergang geprüft werden und die Lampe dementsprechend ein- oder ausgeschaltet wird. Unnötige Kommandos sollen vermieden werden, dazu also gleich mehr.
Mit dieser Überlegung geht es nun zum WHEN-Dialog. Da ich alle 30 Sekunden eine Prüfung möchte, braucht es ein Time Event. Dieses wählen Sie im Dialog aus, worauf gleich die nächste Frage kommt, welche Art von Event es denn sein soll, siehe Abbildung 16.
Abbildung 16: Abfrage, um welches Time Event es geht
Zur Auswahl, welche Art von Time Event es sein soll, stehen:
In unserem Fall möchten wir einen Zeitplan nach Art des Cron-Jobs und lassen die Auswahl erst einmal so stehen. Als Nächstes wählen Sie weiter unten Build, um den Cron-Zeitplan korrekt einzutragen. Hierbei handelt es sich um eine Art Wizard, der mit ihren Angaben einen korrekten Cron-Zeitplan erstellt. Ich erkläre die Reiter von rechts nach links, da es für dieses Beispiel einfacher ist. Der Zeitplan soll jedes Jahr, in jedem Monat, zu jedem Tag, in jeder Stunde und jeder Minute, alle 30 Sekunden beginnend der Sekunde 0 ausgeführt werden, siehe Abbildung 17.
Abbildung 17: Den Cron Zeitplan für alle 30 Sekunden ab Sekunde 0
Demnach sollte der Cron-Zeitplan 0/30 * * * * ? * generiert werden. Damit ist der erste Teil erledigt und kann über den Button Done so übernommen werden.
Als Nächstes müssen Sie unter THEN eine neue Aktion anlegen, in diesem Fall ein Skript mit dem Blockly-Editor, siehe Abbildung 18.
Abbildung 18: Skript hinzufügen
Im neu geöffneten Editor muss man nun die Zeitschaltuhr für unser Aquarium implementieren. Da es hier nun etwas länger dauern würde, den kompletten Editor zu erklären, gebe ich Ihnen einen kurzen Überblick über meine Lösung. Diese ist nicht perfekt und es bedarf noch einigen Verbesserungen, aber das Aquarium wird bei Sonnenaufgang eingeschaltet und bei Sonnenuntergang ausgeschaltet, siehe Abbildung 19.
Abbildung 19: Fertiges Blockly-Skript
Vom Prinzip her ist es ganz einfach, dieses Skript selbst aufzubauen. Auf der linken Seite finde Sie die entsprechenden Bausteine, die Sie Stück für Stück aneinanderreihen. Um z.B. den Logikbaustein für die If-Anweisung zu erweitern, drücken Sie auf das blaue Zahnrad.
Die Farben der einzelnen Blockly-Bausteine repräsentieren die einzelnen Typen der Gruppenbausteine. Einzig für das Prüfen des Sonnenauf- und -untergangs müssen Sie unter Libraries -> Astro den Baustein auswählen.
Im Prinzip dauert es nicht lange, bis Sie dieses einfache Skript „geschrieben“ haben. Speichern Sie es über Save in der oberen rechten Ecke ab. Danach sollte das Skript alle 30 Sekunden ausgeführt werden. Sie können aber auch an der unteren linken Ecke testweise auf Run drücken, um zu prüfen, ob das Skript auch funktioniert.
Funktioniert der Code, wird dieser alle 30 Sekunden nach der eingestellten Regel durchlaufen.
Sie haben gelernt, automatisiert Aufgaben über ein Skript auszuführen. Im aktuellen Beispiel dieses Blogs wurde eine Aquariumlampe angesteuert, jedoch sollte an diesem Beispiel nur gezeigt werden, wie einfach es ist, Aufgaben zu automatisieren. Egal ob es nun eine einfache Lampe ist, oder Sie aber eine komplette Haussteuerung entwickeln wollen. Alles ist mit openHAb möglich, solange Sie es sich vorstellen können. Jetzt liegt es an Ihnen, Ihre Heimautomation umzusetzen.
Weitere Projekte für AZ-Delivery von mir finden Sie unter https://github.com/M3taKn1ght/Blog-Repo.
]]>
1 |
Raspberry Pi Pico W mit aktueller Firmware |
1 |
|
alt |
beliebiger Bausatz mit Chassis und Rädern/Motoren und |
1 |
Breadboard, Jumperkabel, Batteriekasten mit 4 AA-Batterien |
PC mit Thonny, Android Smartphone/Tablet |
|
Smartphone |
Nach der Lektüre der folgenden Quellen war ich in der Lage, im ersten Teil der Blog-Reihe die Anzeige des Temperatur- und Luftfeuchtigkeitssensors DHT20 mit Bluetooth LE auf mein Smartphone bzw. Tablet zu übertragen und im zweiten Teil ein Robot Car mit Bluetooth-Fernsteuerung mit einem zweiten Pico W zu realisieren.
https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html
https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf
https://docs.micropython.org/en/latest/library/bluetooth.html
https://github.com/micropython/micropython/tree/master/examples/bluetooth
Wie gern hätte ich Martin O’Hanlon von der Raspberry Pi Foundation überredet, seine Smartphone App BlueDot für Raspberry Pi mit Python (siehe Blog vom 6.Juni 2022) für den Pico W mit MicroPython zeitnah anzupassen, aber das war leider nicht möglich. Also musste ich wohl oder übel eine eigene App entwickeln, um mein Robot Car mit meinem Smartphone zu steuern. Aber wie schreibt man eine App?
Blogger-Kollege Jürgen Grzesina hat das Vorgehen bereits ausführlich beschrieben und einen ESP32 über das heimische WLAN mit dem Smartphone verbunden (Link zum Blog) und später auch eine Pflanzenbewässerung realisiert (Link zum Block). Aber nach dem Öffnen der Internetseite zum MIT App Inventor war ich doch ratlos und in dem Dschungel der vielen Beispiele wollte ich mich nicht verirren.
Also den empfohlenen Klassiker „Become an App INVENTOR, The Official Guide ..” von Karen Lang und Selim Tezel gekauft und intensiv studiert, d.h. alle Beispiele und Übungen mitgemacht. Das Beispiel „Find the Gold“ in Kapitel 5 hat sofort meine Aufmerksamkeit erregt. Hier wird ein Ball durch ein Labyrinth durch Kippen des Smartphones zum Goldschatz gelenkt. Wäre der Accelerometer Sensor des Smartphone nicht als eine gute Steuerung meines Robot Cars geeignet? Ich entwickle in einem ersten Schritt eine App, in der die Bewegung des Balls auf dem Bildschirm jeweils in die Wertebereiche 0 bis 31 für die y- und x-Achse umgerechnet wird. Das waagerechte Smartphone bedeutet in dem von mir entwickelten Codesystem 16160=Stillstand, die ersten zwei Stellen für die y-Achse (vor und zurück), die nächsten zwei Stellen für die x-Achse (links/rechts). Auf diese Weise nutze ich bei der Datenübertragung den größtmöglichen Bereich für „Signed Integer“.
Dazu zwei Bildschirmfotos vom MIT App Inventor, einmal die Design-Ansicht und dann die Blöcke:
Und der Link meiner exportierten Datei ControllerBall.aia,
Leider gibt es in dem Buch keine Informationen zu Bluetooth LE. Dennoch hat mich das Buch weitergebracht und deshalb kann ich es weiterempfehlen. Den letzten fehlenden Teil an Information habe ich über eine Google Recherche und das YouTube Video von MoThunderz erhalten. Für Bluetooth LE muss man eine sogenannte „Extension“ herunterladen und in sein Projekt einbilden.
Gleich die oberste Zeile der Internetseite https://mit-cml.github.io/extensions/ liefert die gewünschte Erweiterung. Einfach BluetoothLE.aix herunterladen und dann in der Palette des App Inventors ganz links unten importieren.
Hier nun mein erster Versuch einer App zur Bluetooth-Steuerung des Robot Cars mit Raspberry Pi Pico W, zunächst die Bildschirmfotos und dann der Link zum Download.
Und hier die Links für BLE_controller_RobotCar_teil3.aia (Datei für MIT App Inventor) und BLE_controller_RobotCar_teil3.apk (Installationsdatei für Android Smartphone).
Das Programm für das Robot Car aus Teil 2 musste ich im Hauptteil an einer Stelle ändern, weil das Datenformat von der Smartphone App irgendwie den fünfstelligen Code weder als Signed Integer noch als String übertragen hat. Deshalb habe ich die eigentliche Motorsteuerung zunächst auskommentiert und nur den empfangenen Code am Bildschirm anzeigen lassen.
Tatsächlich wurde beim empfangenen Text, z.B. b'16160\x00', der eigentliche Code eingerahmt von weiteren Zeichen und konnte deshalb nicht direkt in eine Integerzahl umgewandelt werden. Die ersten zwei und die letzten vier Zeichen mussten entfernt werden. Das geschieht durch die Zeile
code = str(code)[2:-5] # necessary only for Smartphone App
Danach wird der String in eine Zahl umgewandelt, die dann in die einzelnen Fragmente für die Fahrtanweisungen zerlegt wird.
def on_rx(code): # code is what has been received
code = str(code)[2:-5] # necessary only for Smartphone App
code = int(code)
cy = int(code/1000) # digit 1 and 2
cx = int((code-1000*cy)/10) # digit 3 and 4
cb = code - 1000*cy - 10*cx # digit 5
print("cy = ",cy," cx = ",cx," cb = ",cb) # Print code fragments
motor(cy,cx) # call function motor with 2 parameters
Selbstverständlich wird das MicroPython-Beispielprogramm ble_advertising.py als Modul auf dem Pico W weiterhin benötigt. Ansonsten erhält man bei der Zeile „from ble_advertising import advertising_payload“ eine Fehlermeldung.
Hier der geringfügig gegenüber Teil 2 veränderte Code für das Robot Car zum Download.
Nach dem Testen am PC wird das Programm unter dem Namen main.py auf dem Pico W gespeichert, um die Autostart-Funktion beim Betrieb mit Batterien zu aktivieren. Dann muss der Pico W am Robot Car gestartet werden, um beim Scannen mit der App erkannt zu werden.
„There is always room for improvement” hieß es in meinem Berufsleben immer so schön. Beim Code für das Robot Car könnte man mit einer if-Abfrage zur Länge des Codes (len(code) >5) die o.g. neue Zeile optional einfügen, um beide Möglichkeiten der Steuerung (Smartphone oder zweiter Pico W wie im zweiten Teil der Blog-Reihe) zu ermöglichen. Und man kann bei der Steuerung bis zu drei Buttons mit den Wertigkeiten 1, 2 und 4 als Summand für die fünfte Stelle des Codes einfügen, um z.B. Blinklicht oder Sirene einzuschalten oder zwischen Fernsteuerung und einem autonomen Modus des Robot Cars hin- und her zuschalten.
Wenn das Video nicht angezeigt wird, überprüfen Sie bitte die Cookie-Einstellungen Ihres Browsers.
Viel Spaß bei Ihren Versuchen mit dem Raspberry Pi Pico W und Bluetooth, und probieren Sie mal den MIT App Inventor aus.
]]>und als Modul auf dem Pico W die Datei ble_advertising.py,
die man auf Github unter https://github.com/micropython/micropython/tree/master/examples/bluetooth
findet. Am besten gleich alle „Examples“ als zip-Datei herunterladen.
Für die Verwendung von Bluetooth mit dem Raspberry Pi Pico W benötigen Sie die neueste Firmware, die seit Juni 2023 außer WiFi auch Bluetooth unterstützt. Beim Schreiben des Blog-Beitrags war es diese Datei: micropython-firmware-pico-w-130623.uf2. Dann hält man den BOOTSEL Button gedrückt und verbindet den Pico W über USB mit dem PC. Im Explorer erscheint der Pico wie ein USB-Stick, so dass man die Datei *.uf2 mit „drag and drop“ auf den Pico verschieben kann. Wenige Sekunden später verschwindet das USB-Laufwerk und der Pico W kann mit Thonny programmiert werden.
Als hilfreiche Quellen möchte ich noch einmal die folgenden Internetseiten empfehlen:
https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html
https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf
https://docs.micropython.org/en/latest/library/bluetooth.html
2 |
Raspberry Pi Pico W mit aktueller Firmware |
|
1 |
||
alt |
beliebiger Bausatz mit Chassis und Rädern/Motoren und |
|
2 |
Breadboard, Jumperkabel, Batteriekasten mit 4 AA-Batterien |
|
PC mit Thonny, Android Smartphone/Tablet |
Das Beispielprogramm ble_simple_peripheral.py bildet die Grundlage für das Programm auf dem Robot Car. Wichtig ist, dass das MicroPython-Beispielprogramm ble_advertising.py als Modul auf diesen Pico W kopiert wird. Ansonsten erhält man bei der Zeile „from ble_advertising import advertising_payload“ eine Fehlermeldung. So komisch es klingen mag, das Peripheriegerät bietet den Bluetooth LE-Dienst an, der Controller (s.u. Programmbeispiel auf der Basis von ble_simple_central.py) sucht den Anbieter anhand der UUID und verbindet sich dann.
Eine kurze Erläuterung zu UUID (universally unique identifier). So einmalig, wie der Name klingt, müssen die nicht sein. Wenn Ihr Gerät weit und breit das Einzige ist, können die UUIDs wiederverwendet werden. Ansonsten lässt man sich auf der Internet-Seite https://www.uuidgenerator.net/ neue und einmalige UUIDs generieren.
Die gegenüber den Beispielprogrammen vorgenommenen Änderungen befinden sich zu Beginn beim Importieren und Instanziieren der Pins für die Motorsteuerung und der selbst-definierten Funktion motor(cy,cx) mit zwei Parametern. Im Hauptteil am Ende wird der empfangene Code in seine Bestandteile zerlegt - die ersten zwei Ziffern für vor bzw. zurück, die zweiten beiden Ziffern für links/rechts und die letzte Ziffer für den Joystick-Button. Die Wertebereiche der beiden Joystick-Potis werden „gemapped“ auf den Bereich 0 bis 31; die Mittelposition liegt bei 16. Diesen Wert subtrahiere ich von dem jeweiligen Code-Fragment cy bzw. cx, um Werte zwischen 1 und 15 für Vorwärtsfahrt bzw. -1 bis -15 für Rückwärtsfahrt zu erhalten. Für Kurvenfahrt addiere bzw. subtrahiere ich den halben cx-Wert. Noch nicht verwendet wird die letzte Stelle des Codes (0=Button nicht gedrückt, 1=Button gedrückt); damit könnte man Blinklicht einschalten, hupen oder zwischen Fernsteuerung und autonomen Modus umschalten.
Mein modifiziertes Programm läuft auf Anhieb und die Motoren drehen (eher zufällig) in die richtige Richtung. Wenn die Motoren in die falsche Richtung drehen, muss man entweder die Anschlüsse umpolen oder die Pinbelegung im Code ändern.
Hier der Code für das Robot Car zum Download:
# Robot Car with Raspberry Pi Pico W BLE
# Modified from Official Rasp Pi example here:
# https://github.com/micropython/micropython/tree/master/examples/bluetooth
# by Bernd54Albrecht for AZ-Delivery 2023
import bluetooth
import random
import struct
import time
from ble_advertising import advertising_payload
from micropython import const
from machine import Pin, PWM
## define 3 fragments of the code, idle is 16160
cy = 16 # forward/backward, digit 1 and 2
cx = 16 # left/right, digit 3 and 4
cb = 0 # button, digit 5
# Initialisation of blue LED
led_blue = Pin(14, Pin.OUT)
# Initialisation of motors
vBatt = 6
m1e = PWM(Pin(11))
m11 = Pin(13,Pin.OUT)
m12 = Pin(12,Pin.OUT)
m2e = PWM(Pin(20))
m21 = Pin(19,Pin.OUT)
m22 = Pin(18,Pin.OUT)
m1e.freq(1000)
m2e.freq(1000)
factor = 655.35 * 6/vBatt # max PWM/100 * 6 / vBatt
# self-defined function for 2 motors with PWM
def motor(cy, cx):
y = cy - 15 # forward/backward
x = cx - 15 # left/right
leftWheel = y + 0.5 * x
rightWheel = y - 0.5 * x
if leftWheel > 15:
leftWheel = 15
if leftWheel < -15:
leftWheel = -15
if rightWheel > 15:
rightWheel = 15
if rightWheel < -15:
rightWheel = -15
if leftWheel < -2:
m11.off()
m12.on()
elif leftWheel > 2:
m11.on()
m12.off()
else:
m11.off()
m12.off()
# m1e.duty_u16(0)
if rightWheel < -2:
m21.off()
m22.on()
elif rightWheel > 2:
m21.on()
m22.off()
else:
m21.off()
m22.off()
# m2e.duty_u16(0)
leftPWM = int(factor * (25 + 5*abs(leftWheel)))
print("leftWheel = ",leftWheel," leftPWM = ", leftPWM)
rightPWM = int(factor * (25 + 5*abs(rightWheel)))
print("rightWheel = ",rightWheel," rightPWM = ", rightPWM)
m1e.duty_u16(leftPWM)
m2e.duty_u16(rightPWM)
## taken from ble_simple_peripheral.py
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_FLAG_READ = const(0x0002)
_FLAG_WRITE_NO_RESPONSE = const(0x0004)
_FLAG_WRITE = const(0x0008)
_FLAG_NOTIFY = const(0x0010)
_UART_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_TX = (
bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_READ | _FLAG_NOTIFY,
)
_UART_RX = (
bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE,
)
_UART_SERVICE = (
_UART_UUID,
(_UART_TX, _UART_RX),
)
class BLESimplePeripheral:
def __init__(self, ble, name="mpy-uart"):
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq)
((self._handle_tx, self._handle_rx),) = self._ble.gatts_register_services((_UART_SERVICE,))
self._connections = set()
self._write_callback = None
self._payload = advertising_payload(name=name, services=[_UART_UUID])
self._advertise()
def _irq(self, event, data):
# Track connections so we can send notifications.
if event == _IRQ_CENTRAL_CONNECT:
conn_handle, _, _ = data
print("New connection", conn_handle)
self._connections.add(conn_handle)
elif event == _IRQ_CENTRAL_DISCONNECT:
conn_handle, _, _ = data
print("Disconnected", conn_handle)
self._connections.remove(conn_handle)
# Start advertising again to allow a new connection.
self._advertise()
elif event == _IRQ_GATTS_WRITE:
conn_handle, value_handle = data
value = self._ble.gatts_read(value_handle)
if value_handle == self._handle_rx and self._write_callback:
self._write_callback(value)
def send(self, data):
for conn_handle in self._connections:
self._ble.gatts_notify(conn_handle, self._handle_tx, data)
def is_connected(self):
return len(self._connections) > 0
def _advertise(self, interval_us=500000):
print("Starting advertising")
self._ble.gap_advertise(interval_us, adv_data=self._payload)
def on_write(self, callback):
self._write_callback = callback
# This is the MAIN LOOP
def demo(): # This part modified to control Robot Car
ble = bluetooth.BLE()
p = BLESimplePeripheral(ble)
def on_rx(code): # code is what has been received
code = int(code)
cy = int(code/1000) # digit 1 and 2
cx = int((code-1000*cy)/10) # digit 3 and 4
cb = code - 1000*cy - 10*cx # digit 5
print("cy = ",cy," cx = ",cx," cb = ",cb) # Print code fragments
motor(cy,cx) # call function motor with 2 parameters
p.on_write(on_rx)
if __name__ == "__main__":
demo()
Der Programm-Code für den Controller beruht auf dem Beispielprogramm ble_simple_central.py. Hinzugefügt werden aus dem Modul machine die Klassen Pin und PWM, mit denen das Joystick-Modul initialisiert und abgefragt wird. Mit poti1 = ADC(26).read_u16() und poti2 = ADC(27).read_u16() werden die Zahlenwerte für die x- und y-Richtung zwischen 0 und 65535 ermittelt. Durch ganzzahlige Division durch 2048 erhält man den Wertebereich 0 bis 31. Die y-Werte werden mit 1000, die x-Werte mit 10 multipliziert, um die ersten 4 Stellen des fünfstelligen Codes zu erhalten. Mit der letzten Stelle wird der Zustand des Buttons addiert.
Beispiele: Code 16161 bedeutet Leerlauf und Button gedrückt,
Code 31160 bedeutet schnellste Geradeausfahrt und Button nicht gedrückt,
Code 25310 bedeutet moderate Geschwindigkeit und scharfe Rechtskurve,
Code 8080 bedeutet moderate Rückwärtsfahrt und leichte Linkskurve (die Null
am Anfang wird zwangsläufig weggelassen).
Dieser Code wird als String gesendet und im Programm des Robot Cars decodiert. Dazu wird er dort wieder in eine Integerzahl verwandelt und in die drei Fragmente zerlegt (s.o.)
Und hier der Code für den Controller zum Download:
# PicoW_BLE_Robot_Controller.py
# Joystick with two 10K potentiometers on ADC0 and ADC1
# Modified from Official Rasp Pi example here:
# https://github.com/micropython/micropython/tree/master/examples/bluetooth
# by Bernd54Albrecht for AZ-Delivery 2023
import bluetooth
import random
import struct
import time
import micropython
from ble_advertising import decode_services, decode_name
from micropython import const
# Additional code for joystick
from machine import ADC, Pin
button = Pin(15, Pin.IN, Pin.PULL_UP)
## taken from ble_simple_central.py
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_IRQ_GATTS_READ_REQUEST = const(4)
_IRQ_SCAN_RESULT = const(5)
_IRQ_SCAN_DONE = const(6)
_IRQ_PERIPHERAL_CONNECT = const(7)
_IRQ_PERIPHERAL_DISCONNECT = const(8)
_IRQ_GATTC_SERVICE_RESULT = const(9)
_IRQ_GATTC_SERVICE_DONE = const(10)
_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
_IRQ_GATTC_DESCRIPTOR_RESULT = const(13)
_IRQ_GATTC_DESCRIPTOR_DONE = const(14)
_IRQ_GATTC_READ_RESULT = const(15)
_IRQ_GATTC_READ_DONE = const(16)
_IRQ_GATTC_WRITE_DONE = const(17)
_IRQ_GATTC_NOTIFY = const(18)
_IRQ_GATTC_INDICATE = const(19)
_ADV_IND = const(0x00)
_ADV_DIRECT_IND = const(0x01)
_ADV_SCAN_IND = const(0x02)
_ADV_NONCONN_IND = const(0x03)
_UART_SERVICE_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_RX_CHAR_UUID = bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_TX_CHAR_UUID = bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
class BLESimpleCentral:
def __init__(self, ble):
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq)
self._reset()
def _reset(self):
# Cached name and address from a successful scan.
self._name = None
self._addr_type = None
self._addr = None
# Callbacks for completion of various operations.
# These reset back to None after being invoked.
self._scan_callback = None
self._conn_callback = None
self._read_callback = None
# Persistent callback for when new data is notified from the device.
self._notify_callback = None
# Connected device.
self._conn_handle = None
self._start_handle = None
self._end_handle = None
self._tx_handle = None
self._rx_handle = None
def _irq(self, event, data):
if event == _IRQ_SCAN_RESULT:
addr_type, addr, adv_type, rssi, adv_data = data
if adv_type in (_ADV_IND, _ADV_DIRECT_IND) and _UART_SERVICE_UUID in decode_services(
adv_data
):
# Found a potential device, remember it and stop scanning.
self._addr_type = addr_type
self._addr = bytes(
addr
) # Note: addr buffer is owned by caller so need to copy it.
self._name = decode_name(adv_data) or "?"
self._ble.gap_scan(None)
elif event == _IRQ_SCAN_DONE:
if self._scan_callback:
if self._addr:
# Found a device during the scan (and the scan was explicitly stopped).
self._scan_callback(self._addr_type, self._addr, self._name)
self._scan_callback = None
else:
# Scan timed out.
self._scan_callback(None, None, None)
elif event == _IRQ_PERIPHERAL_CONNECT:
# Connect successful.
conn_handle, addr_type, addr = data
if addr_type == self._addr_type and addr == self._addr:
self._conn_handle = conn_handle
self._ble.gattc_discover_services(self._conn_handle)
elif event == _IRQ_PERIPHERAL_DISCONNECT:
# Disconnect (either initiated by us or the remote end).
conn_handle, _, _ = data
if conn_handle == self._conn_handle:
# If it was initiated by us, it'll already be reset.
self._reset()
elif event == _IRQ_GATTC_SERVICE_RESULT:
# Connected device returned a service.
conn_handle, start_handle, end_handle, uuid = data
print("service", data)
if conn_handle == self._conn_handle and uuid == _UART_SERVICE_UUID:
self._start_handle, self._end_handle = start_handle, end_handle
elif event == _IRQ_GATTC_SERVICE_DONE:
# Service query complete.
if self._start_handle and self._end_handle:
self._ble.gattc_discover_characteristics(
self._conn_handle, self._start_handle, self._end_handle
)
else:
print("Failed to find uart service.")
elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:
# Connected device returned a characteristic.
conn_handle, def_handle, value_handle, properties, uuid = data
if conn_handle == self._conn_handle and uuid == _UART_RX_CHAR_UUID:
self._rx_handle = value_handle
if conn_handle == self._conn_handle and uuid == _UART_TX_CHAR_UUID:
self._tx_handle = value_handle
elif event == _IRQ_GATTC_CHARACTERISTIC_DONE:
# Characteristic query complete.
if self._tx_handle is not None and self._rx_handle is not None:
# We've finished connecting and discovering device, fire the connect callback.
if self._conn_callback:
self._conn_callback()
else:
print("Failed to find uart rx characteristic.")
elif event == _IRQ_GATTC_WRITE_DONE:
conn_handle, value_handle, status = data
print("TX complete")
elif event == _IRQ_GATTC_NOTIFY:
conn_handle, value_handle, notify_data = data
if conn_handle == self._conn_handle and value_handle == self._tx_handle:
if self._notify_callback:
self._notify_callback(notify_data)
# Returns true if we've successfully connected and discovered characteristics.
def is_connected(self):
return (
self._conn_handle is not None
and self._tx_handle is not None
and self._rx_handle is not None
)
# Find a device advertising the environmental sensor service.
def scan(self, callback=None):
self._addr_type = None
self._addr = None
self._scan_callback = callback
self._ble.gap_scan(2000, 30000, 30000)
# Connect to the specified device (otherwise use cached address from a scan).
def connect(self, addr_type=None, addr=None, callback=None):
self._addr_type = addr_type or self._addr_type
self._addr = addr or self._addr
self._conn_callback = callback
if self._addr_type is None or self._addr is None:
return False
self._ble.gap_connect(self._addr_type, self._addr)
return True
# Disconnect from current device.
def disconnect(self):
if self._conn_handle is None:
return
self._ble.gap_disconnect(self._conn_handle)
self._reset()
# Send data over the UART
def write(self, v, response=False):
if not self.is_connected():
return
self._ble.gattc_write(self._conn_handle, self._rx_handle, v, 1 if response else 0)
# Set handler for when data is received over the UART.
def on_notify(self, callback):
self._notify_callback = callback
def demo(): # This is the MAIN LOOP
ble = bluetooth.BLE()
central = BLESimpleCentral(ble)
not_found = False
def on_scan(addr_type, addr, name):
if addr_type is not None:
print("Found peripheral:", addr_type, addr, name)
central.connect()
else:
nonlocal not_found
not_found = True
print("No peripheral found.")
central.scan(callback=on_scan)
# Wait for connection...
while not central.is_connected():
time.sleep_ms(100)
if not_found:
return
print("Connected")
with_response = False
# Modified section for joystick and calculation of code
while central.is_connected():
try:
# Read the raw potentiometer value from specified potentiometer
poti1 = ADC(26).read_u16()
poti2 = ADC(27).read_u16()
y = int(poti1/2048)* 1000
x = int(poti2/2048)*10
z = abs(button.value()-1)
code = str(y + x + z)
print("y = ", y)
print("x = ", x)
central.write(code, with_response)
except:
print("TX failed")
time.sleep_ms(1000)
time.sleep_ms(400 if with_response else 30)
print("Disconnected")
if __name__ == "__main__":
demo()
Nach dem Testen am PC werden die beiden Programme jeweils unter dem Namen main.py auf den Pico Ws abgespeichert, um die Autostart-Funktion beim Betrieb mit Batterien zu aktivieren. Dabei muss der Pico W am Robot Car einige Sekunden vorher gestartet werden, um beim Einschalten des Controllers bereits den BLE-Dienst anzubieten.
Sollte das Video nicht angezeigt werden, überprüfen Sie bitte die Cookie-Einstellungen Ihres Browsers.
Viel Spaß bei Ihren Versuchen mit dem Raspberry Pi Pico W und Bluetooth.
Hier geht's mit Teil 3 der Blogserie weiter: Teil 3 - Robot Car mit Smartphone App
]]>