Mit ESP8266, Micropython und dem BME280 einen Datenlogger basteln

Nachdem wir in Teil 1 ja schon die Daten eines BME280 Sensors (Temperatur, Luftdruck, Luftfeuchte) von einem Raspberry Pi aus ins Internet übertragen und schön bunt auswerten, folgt nun Teil 2: Diesmal nehmen wir einen sehr günstigen ESP8266 Microcontroller (~3 Euro) mit Micropython!

Zuallererst kaufen wir uns ein fertiges ESP8266 Board – das hat neben dem ESP8266 noch einen USB-Seriell Wandler, einen (oft stromfressenden!) Step-Down Converter (von 5V USB auf 3.3V für den ESP8266), eine LED und einen USB-Anschluß mit dabei. Testen werde ich hier zwei Stück, und zwar:
NodeMCU Lua Amica Modul V2 ESP8266 ESP-12E Wifi Wifi Development Board mit CP2102
D1 Mini NodeMcu mit ESP8266-12E WLAN Module für Arduino, 100% WeMos kompatibel

Beide Boards kosten 5.99€ (wenn man ein paar Wochen Zeit hat kann man sie bei aliexpress für die Hälfte bestellen!) und der große Unterschied ist dass man beim Wemos die Steckerleisten selbst anlöten muss. Dafür hat das Wemos einen deutlich stromsparenderen Step-Down Converter.

Als allererstes müssen wir mal Micropython auf die beiden Boards bringen. Downloaden können wir das hier: micropython.org

Dann brauchen wir noch python3, pip3, esptool und rshell – unter (Ubuntu) Linux sollten die folgenden Befehle hier schnell zum Ziel führen:

apt-get install python3 python3-pip
pip3 install esptool rshell

Nachdem wir das gemacht haben stecken wir das Board unsere Wahl an den Computer an. Mittels ‚dmesg‘ sollten wir sehen welchen Anschluß es zugewiesen bekommt. Bei mir ist es eigentlich immer ‚/dev/ttyUSB0‘ gewesen.

Wenn wir nun probieren zuerst den Speicher zu leeren und danach Micropython aufzuspielen so können wir das nur wenn das Board im Bootloader Modus ist. Beides Boards versuchen diesen Modus automatisch zu aktivieren wenn man Kommandos überträgt (die gleiche Technik wie bei Arduino) aber das klappt nicht unbedingt immer. Deshalb muss man beim NodeMCU Board den FLASH Button halten und danach das Board neu anstecken oder mittels Reset-Button neu starten. Dann kann man den Flash Button loslassen denn man ist nun im Bootloader Modus. Beim Wemos ist es noch einen Tick komplizierter denn hier gibt es einfach mal keinen Flash Button. Statt dem Flash Button muss man einfach den Pin GPI0/D0 mit einem kleinen Kabel mit GND verbinden!

Nun zum flashen: Diese beiden Befehle leeren erst den Flash-Speicher des ESP8266 und übertragen dann Micropython drauf:

esptool.py --port /dev/ttyUSB0 erase_flash
esptool.py --port /dev/ttyUSB0 write_flash -fs 4MB -fm dout 0x0 /home/ingres/esp8266-20171101-v1.9.3.bin

Nun können wir uns entweder ganz normal mittels Putty oder Screen auf die REPL von Micropython verbinden, oder aber wir nehmen ‚rshell‘, welches den Vorteil hat dass es auch Dateien übertragen kann. Und da Micropython per Default die Datei ‚main.py‘ automatisch beim Starten des Boards startet müssen wir unser Programm nur als ‚main.py‘ übertragen. So einfach 🙂

Hier also der Befehl um rshell zu starten und die main.py auf das Board zu senden. Mittels ‚repl‘ kommt man von rshell aus auf die Micropython Kommandokonsole. Mit Strg+X kommt man wieder aus der REPL raus.

rshell --buffer-size=30 -p /dev/ttyUSB0
cp main.py /pyboard

Das einzige was jetzt noch fehlt ist das Programm ‚main.py‘ und die richtige Verkabelung. Hier also main.py:

# Using parts of the following scripts:
# 1. https://www.raspberrypi-spy.co.uk/2016/07/using-bme280-i2c-temperature-pressure-sensor-in-python/
# 2. http://ecorov.com/2017/08/03/i2c-gy-91.html
import time
import ustruct
import machine
import network
import usocket

# Globals
debug = 1
sensorid = 3
sensoraddr = 0x76
sendseconds = 60
secondstrywificonnect = 10
REG_DATA = 0xF7
REG_CONTROL = 0xF4
REG_CONFIG  = 0xF5
REG_CONTROL_HUM = 0xF2
REG_HUM_MSB = 0xFD
REG_HUM_LSB = 0xFE
OVERSAMPLE_TEMP = 2
OVERSAMPLE_PRES = 2
MODE = 1

if debug:
	print(str(time.ticks_ms()) + ': ***STARTUP***')
	print(str(time.ticks_ms()) + ': sensorid: ' + str(sensorid))
	print(str(time.ticks_ms()) + ': sensoraddr: ' + str(sensoraddr))
	print(str(time.ticks_ms()) + ': sendseconds: ' + str(sendseconds))
	print(str(time.ticks_ms()) + ': secondstrywificonnect: ' + str(secondstrywificonnect))
	print(str(time.ticks_ms()) + ': REG_DATA: ' + str(REG_DATA))
	print(str(time.ticks_ms()) + ': REG_CONTROL: ' + str(REG_CONTROL))
	print(str(time.ticks_ms()) + ': REG_CONFIG: ' + str(REG_CONFIG))
	print(str(time.ticks_ms()) + ': REG_CONTROL_HUM: ' + str(REG_CONTROL_HUM))
	print(str(time.ticks_ms()) + ': REG_HUM_MSB: ' + str(REG_HUM_MSB))
	print(str(time.ticks_ms()) + ': REG_HUM_LSB: ' + str(REG_HUM_LSB))
	print(str(time.ticks_ms()) + ': OVERSAMPLE_TEMP: ' + str(OVERSAMPLE_TEMP))
	print(str(time.ticks_ms()) + ': OVERSAMPLE_PRES: ' + str(OVERSAMPLE_PRES))
	print(str(time.ticks_ms()) + ': MODE: ' + str(MODE))

# Activate a time which will reset us in sendseconds if we're hanging somewhere!
if debug:
	print(str(time.ticks_ms()) + ': Activating Timer')
tim = machine.Timer(-1) 
tim.init(period = sendseconds * 1000, mode = machine.Timer.ONE_SHOT, callback = lambda t:machine.reset())

# Activate the internal RTC (we don't need the time, but we want to know when a minute has passed!)
if debug:
	print(str(time.ticks_ms()) + ': Activating RTC')
rtc = machine.RTC()
# create a RTC alarm that expires after 60 seconds
rtc.irq(trigger = rtc.ALARM0, wake = machine.DEEPSLEEP)
rtc.alarm(rtc.ALARM0, sendseconds * 1000)
if debug:
	print(str(time.ticks_ms()) + ': RTC Time now: ' + '{:02}'.format(rtc.datetime()[2]) + '.' + '{:02}'.format(rtc.datetime()[1]) + '.' + '{:04}'.format(rtc.datetime()[0]) + ' ' + '{:02}'.format(rtc.datetime()[4]) + ':' + '{:02}'.format(rtc.datetime()[5]) + ':' + '{:02}'.format(rtc.datetime()[6]))

# Any exception will send us into deepsleep (where our rtc will wake us!)
try:
	if debug:
		print(str(time.ticks_ms()) + ': Trying to connect')
	# Connect to WiFi -> Get interfaces
	sta_if = network.WLAN(network.STA_IF)
	ap_if = network.WLAN(network.AP_IF)
	# Deactivate access point, we're station only
	ap_if.active(False)
	# Now connect
	connectcount = 0
	if not sta_if.isconnected():
		sta_if.active(True)
		sta_if.connect('myWifi', 'myPass')
		while not sta_if.isconnected():
			connectcount = connectcount + 1
			time.sleep(1)
			if debug:
				print(str(time.ticks_ms()) + ': Connect try: ' + str(connectcount))
			if connectcount > secondstrywificonnect:
				# We didn't connect after secondstrywificonnect seconds. Let's sleep
				if debug:
					print(str(time.ticks_ms()) + ': Connect failed after ' + str(connectcount) + ' seconds sleep. Going to deepsleep')
				machine.deepsleep()
	
	# Startup I2C
	if debug:
		print(str(time.ticks_ms()) + ': Starting I2C')
	i2c = machine.I2C(scl = machine.Pin(5), sda = machine.Pin(4), freq = 100000)
	
	# Set Oversampling
	if debug:
		print(str(time.ticks_ms()) + ': Setting BME280 Oversampling')
	OVERSAMPLE_HUM = 2
	i2c.writeto_mem(sensoraddr, REG_CONTROL_HUM, bytearray([OVERSAMPLE_HUM]))
	control = OVERSAMPLE_TEMP << 5 | OVERSAMPLE_PRES << 2 | MODE
	i2c.writeto_mem(sensoraddr, REG_CONTROL, bytearray([control]))
	
	# Read Calibration data from sensor
	if debug:
		print(str(time.ticks_ms()) + ': Getting BME280 Calibration data')
	cal1 = i2c.readfrom_mem(sensoraddr, 0x88, 24)
	cal2 = i2c.readfrom_mem(sensoraddr, 0xA1, 1)
	cal3 = i2c.readfrom_mem(sensoraddr, 0xE1, 7)
	
	# Convert bytes to words
	dig_T1 = ustruct.unpack('H', cal1[0:2])[0]
	dig_T2 = ustruct.unpack('h', cal1[2:4])[0]
	dig_T3 = ustruct.unpack('h', cal1[4:6])[0]
	dig_P1 = ustruct.unpack('H', cal1[6:8])[0]
	dig_P2 = ustruct.unpack('h', cal1[8:10])[0]
	dig_P3 = ustruct.unpack('h', cal1[10:12])[0]
	dig_P4 = ustruct.unpack('h', cal1[12:14])[0]
	dig_P5 = ustruct.unpack('h', cal1[14:16])[0]
	dig_P6 = ustruct.unpack('h', cal1[16:18])[0]
	dig_P7 = ustruct.unpack('h', cal1[18:20])[0]
	dig_P8 = ustruct.unpack('h', cal1[20:22])[0]
	dig_P9 = ustruct.unpack('h', cal1[22:24])[0]
	dig_H1 = ustruct.unpack('B', cal2[0:1])[0]
	dig_H2 = ustruct.unpack('h', cal3[0:2])[0]
	dig_H3 = ustruct.unpack('B', cal3[2:3])[0]
	dig_H4 = ustruct.unpack('b', cal3[3:4])[0]
	dig_H4 = (dig_H4 << 24) >> 20
	dig_H4 = dig_H4 | (ustruct.unpack('b', cal3[4:5])[0] & 0x0F)
	dig_H5 = ustruct.unpack('b', cal3[5:6])[0]
	dig_H5 = (dig_H5 << 24) >> 20
	dig_H5 = dig_H5 | (ustruct.unpack('B', cal3[4:5])[0] >> 4 & 0x0F)
	dig_H6 = ustruct.unpack('b', cal3[6:7])[0]
	
	# Wait in ms (Datasheet Appendix B: Measurement time and current calculation)
	if debug:
		print(str(time.ticks_ms()) + ': Sleeping for BME280 to be ready')
	wait_time = 1.25 + (2.3 * OVERSAMPLE_TEMP) + ((2.3 * OVERSAMPLE_PRES) + 0.575) + ((2.3 * OVERSAMPLE_HUM)+0.575)
	time.sleep(wait_time/1000)
	
	# Read temperature/pressure/humidity
	if debug:
		print(str(time.ticks_ms()) + ': Reading BME280')
	data = i2c.readfrom_mem(sensoraddr, REG_DATA, 8)
	pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
	temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
	hum_raw = (data[6] << 8) | data[7]
	
	# Refine temperature
	if debug:
		print(str(time.ticks_ms()) + ': Refine temperature')	
	var1 = ((((temp_raw>>3)-(dig_T1<<1)))*(dig_T2)) >> 11
	var2 = (((((temp_raw>>4) - (dig_T1)) * ((temp_raw>>4) - (dig_T1))) >> 12) * (dig_T3)) >> 14
	t_fine = var1 + var2
	temperature = float(((t_fine * 5) + 128) >> 8)
	
	# Refine pressure and adjust for temperature
	if debug:
		print(str(time.ticks_ms()) + ': Refine pressure')	
	var1 = t_fine / 2.0 - 64000.0
	var2 = var1 * var1 * dig_P6 / 32768.0
	var2 = var2 + var1 * dig_P5 * 2.0
	var2 = var2 / 4.0 + dig_P4 * 65536.0
	var1 = (dig_P3 * var1 * var1 / 524288.0 + dig_P2 * var1) / 524288.0
	var1 = (1.0 + var1 / 32768.0) * dig_P1
	if var1 == 0:
		pressure = 0
	else:
		pressure = 1048576.0 - pres_raw
		pressure = ((pressure - var2 / 4096.0) * 6250.0) / var1
		var1 = dig_P9 * pressure * pressure / 2147483648.0
		var2 = pressure * dig_P8 / 32768.0
		pressure = pressure + (var1 + var2 + dig_P7) / 16.0
	
	# Refine humidity
	if debug:
		print(str(time.ticks_ms()) + ': Refine humidity')	
	humidity = t_fine - 76800.0
	humidity = (hum_raw - (dig_H4 * 64.0 + dig_H5 / 16384.0 * humidity)) * (dig_H2 / 65536.0 * (1.0 + dig_H6 / 67108864.0 * humidity * (1.0 + dig_H3 / 67108864.0 * humidity)))
	humidity = humidity * (1.0 - dig_H1 * humidity / 524288.0)
	if humidity > 100:
		humidity = 100
	elif humidity < 0:
		humidity = 0
	
	# Data is ready
	temperature = temperature / 100.0
	pressure = pressure / 100.0
	humidity = humidity
	if debug:
		print(str(time.ticks_ms()) + ': Data fetched: temp ' + str(temperature) + ' - press ' + str(pressure) + ' - hum ' + str(humidity))
	
	# Send data to the Internet, a Post Request with http - we don't use SSL here!
	content = b'sensorid=' + str(sensorid) + '&temp=' + str(temperature) + '&press=' + str(pressure) + '&hum=' + str(humidity) + '&password=zzz'
	
	if debug:
		print(str(time.ticks_ms()) + ': Connecting to website')
	addr_info = usocket.getaddrinfo("meinserver.de", 80)
	addr = addr_info[0][-1]
	sock = usocket.socket()
	sock.connect(addr)
	sock.send(b'POST /tempsensor.php HTTP/1.1\r\n')
	sock.send(b'Host: meinserver.de\r\n')
	sock.send(b'Content-Type: application/x-www-form-urlencoded\r\n')
	sock.send(b'Content-Length: ' + str(len(content)) + '\r\n')
	sock.send(b'\r\n')
	if debug:
		print(str(time.ticks_ms()) + ': Sending: ' + str(content))
	sock.send(content)
	sock.send(b'\r\n\r\n')
	if debug:
		print(str(time.ticks_ms()) + ': Answer: ' + str(sock.recv(1000)))	
	sock.close()
	
	# Go to deep sleep (Device starts from reset again!) - Make sure that ESP8266 GPIO16 is connected to the RST Pin!
	if debug:
		print(str(time.ticks_ms()) + ': All done. DEEPSLEEP')
	machine.deepsleep()
except:
	if debug:
		print(str(time.ticks_ms()) + ': Exception happend. DEEPSLEEP')
	machine.deepsleep()

Der Einfachheit halber gibts die Datei hier zum runterladen: main.py

Auf dem Server nehmen wir unser bekanntes PHP-Script ‚tempsensor.php‘ von Teil1. Die Datenbank ist dort auch beschrieben. Die Sensorid für den neuen Sensor 2 können wir wie folgt hinzufügen:

INSERT INTO `sensors` (`id`, `name`, `description`) VALUES
(2, 'outside living room', 'on the terrace right before the window to my computer');

Nun noch die Anschlüsse:

  1. Damit der ESP8266 aus dem Deepsleep aufwacht muss der WAKE-Pin (D0) mit dem RST Pin fest verbunden werden.
  2. VCC vom BME280 an den 3.3V Pin
  3. GND vom BME280 an den GND Pin
  4. SCL vom BME280 an D1
  5. SDA vom BME280 an D2

Fertig! So einfach 🙂

Was ich nun noch bieten kann sind einmal die Debug-Ausgaben mit Timing, damit man weiß wie lange die Phase ist in der der ESP8266 viel Strom zieht und wie lange er im Deepsleep ist, dies hier ist einmal ein kompletter Neustart und ein Start wo das WLAN schonmal connected war und deshalb schneller connected ist:
(Um diese Debug-Ausgaben zu bekommen muss man das Board einfach an den Computer anstecken und auf dem seriellen Port per Putty oder screen oder minicom hören. Übrigens geht das neu übertragen von Programmcode per rshell nicht mehr da das Programm ja durchgehend läuft und deshalb die rshell Kommandos nicht durchkommen. Man muss also Micropython neu flashen wenn man die main.py austauschen möchte!)

956: ***STARTUP***
960: sensorid: 3
965: sensoraddr: 118
970: sendseconds: 60
975: secondstrywificonnect: 10
980: REG_DATA: 247
985: REG_CONTROL: 244
990: REG_CONFIG: 245
995: REG_CONTROL_HUM: 242
1000: REG_HUM_MSB: 253
1005: REG_HUM_LSB: 254
1010: OVERSAMPLE_TEMP: 2
1015: OVERSAMPLE_PRES: 2
1020: MODE: 1
1025: Activating Timer
1029: Activating RTC
1034: RTC Time now: 07.10.2018 18:21:55
1062: Trying to connect
#5 ets_task(4020f474, 28, 3fff8df8, 10)
2229: Connect try: 1
3237: Connect try: 2
4292: Connect try: 3
5300: Connect try: 4
6309: Connect try: 5
6318: Starting I2C
6322: Setting BME280 Oversampling
6327: Getting BME280 Calibration data
6343: Sleeping for BME280 to be ready
6363: Reading BME280
6369: Refine temperature
6373: Refine pressure
6377: Refine humidity
6381: Data fetched: temp 23.76 - press 965.002 - hum 46.4372
6399: Connecting to website
6486: Sending: b'sensorid=3&temp=23.76&press=965.002&hum=46.4372&password=zzz'
6492: Answer: b'HTTP/1.1 200 OK\r\nDate: Sun, 07 Oct 2018 16:23:28 GMT\r\nServer: Apache\r\nContent-Length: 5\r\nContent-Type: text/html; charset=UTF-8\r\n\r\nDone.'
6768: All done. DEEPSLEEP

948: ***STARTUP***
952: sensorid: 3
957: sensoraddr: 118
963: sendseconds: 60
968: secondstrywificonnect: 10
973: REG_DATA: 247
978: REG_CONTROL: 244
983: REG_CONFIG: 245
988: REG_CONTROL_HUM: 242
993: REG_HUM_MSB: 253
997: REG_HUM_LSB: 254
1002: OVERSAMPLE_TEMP: 2
1007: OVERSAMPLE_PRES: 2
1012: MODE: 1
1017: Activating Timer
1021: Activating RTC
1025: RTC Time now: 07.10.2018 18:22:56
1053: Trying to connect
2059: Connect try: 1
3067: Connect try: 2
4075: Connect try: 3
4082: Starting I2C
4086: Setting BME280 Oversampling
4094: Getting BME280 Calibration data
4110: Sleeping for BME280 to be ready
4130: Reading BME280
4136: Refine temperature
4141: Refine pressure
4145: Refine humidity
4149: Data fetched: temp 23.68 - press 965.0 - hum 46.6988
4167: Connecting to website
4227: Sending: b'sensorid=3&temp=23.68&press=965.0&hum=46.6988&password=zzz'
4233: Answer: b'HTTP/1.1 200 OK\r\nDate: Sun, 07 Oct 2018 16:24:24 GMT\r\nServer: Apache\r\nContent-Length: 5\r\nContent-Type: text/html; charset=UTF-8\r\n\r\nDone.'
4444: All done. DEEPSLEEP

Grob können wir also sagen dass wir 6 Sekunden High Power haben und danach ca. 54 Sekunden Deepsleep. Während der High Power Phase brauchen beide Boards ca. 55 mA mit bis zu 600 mA Peak. Das NodeMCU sleept mit 1.3 mA, das Wemos D1 Mini mit 175 µA. Senden wir alle 10 Minuten so ist das ein durchschnittlicher Stromverbrauch (bei 5V) von 1.837 mA beim NodeMCU und 0.723 mA beim Wemos. Eine (theoretische) 5V Batterie mit 2000 mAh würde also beim NodeMCU für ca. 1088 Stunden halten, beim Wemos für ca. 2766 Stunden, also 45 Tage oder 115 Tage.
Mit der theoretischen 5V, 2000 mAh Batterie meine ich übrigens wenn man z.B. 4x 1.2V Akkus (oder 1.5V Batterien) mit ca. 2000 mAh Stunden hintereinander schaltet. So erhält man bei Batterien irgendwas zwischen 6.8V und 4.8V, bei Akkus zwischen 5.5V und 4V mit eben jenen 2000 mAh.

Hier meine Berechnungen, grob überschlagen aber hoffentlich grob richtig:

Alle 10 Minuten senden beim NodeMCU:
00.6 Sekunden 55 mA -> 0.06 Sekunden mit 0,275 Watt
59.4 Sekunden 1.3 mA -> 59.4 Sekunden mit 0,0065 Watt
->
((55 * 0,6) + (59,4 * 1,3)) / 60
(33 + 77,22) / 60 => 1.837 mA => 0.09185 Watt

Alle 10 Minuten senden beim Wemos D1:
00.6 Sekunden 55 mA -> 0.06 Sekunden mit 0,275 Watt
59.4 Sekunden 0.175 mA -> 59.4 Sekunden mit 0,000875 Watt
->
((55 * 0,6) + (59,4 * 0,175)) / 60
(33 + 10,395) / 60 => 0.72325 mA => 0.00361625 Watt

Und hier gibt es jetzt noch ein hübsches Bild der Messungen der letzten drei Tage innen und außen:

In Teil 3 schauen wir übrigens meinem Stromzähler genau auf die Finger!


Kommentare

2 Antworten zu „Mit ESP8266, Micropython und dem BME280 einen Datenlogger basteln“

  1. […] Und weils soviel Spaß gemacht hat, und ich noch eine Sensor für außen brauche, geht es hier weiter: Mit ESP8266, Micropython und dem BME280 einen Datenlogger basteln […]

  2. […] Teil 1 darum ging wie man Temperatur, Luftdruck und Luftfeuchte mit dem Raspberry Pi misst, und in Teil 2 das ganze mit dem ESP8266 gemacht wurde, bauen wir auf dem vorhandenem Wissen auf und gehen nun zum […]

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Time limit is exhausted. Please reload CAPTCHA.