In the previous contributions AZ-Onboard with BH1750 and AZ onboard with SHT30 We had dealt with the light intensity sensor BH1750 and the SHT30, which measures temperature and relative humidity. Today the third sensor of the kit is coming, the SGP30. He is responsible for measuring the air quality indoors. The acronym IAQ is derived from this property, Indoor Air Quality. For this sensor we will knit a micropython module.
With the expansion of the AZ outboard from the second episode, we can also control a relay and two LEDs. Three GPIOs still remain free. Using LED and relay, let's take a look at how to switch a switch with a time delay in the program (staircase machine, monoflop) and an input switch with hysteresis, i.e. with a staggered single-out sweat. We also try to build a nice little gadget with the BH1750. Have you ever turned on an LED with a lighter and then simply blown out later? We discuss all of these topics in this new episode from the series
Micropython on the ESP32, ESP8266 and Raspberry Pi Pico
today
The AZ outboard in full expansion
All three satellite boards for the AZ-Eoneboard have the same pin assignment and are therefore interchangeable. The order of the pins on the OLED deviates from it. When connecting to jumpling cables, you have to be careful not to damage the boards. How you can lead the GPIOs unused by the AZ onboard. Is im second part described. For the I2C bus expansion, I cut a small hole grid board with 4 pins width and 12 pins length and provided myself with an angled plug and a angled socket strip at the ends. In between I have soldered two straight, four -pin socket strips. In addition to the three standard sensors, I can also connect other satellites, such as the OLED display. The contacts are connected along.
Figure 1: AZ onboard with sensors, relays and LEDs
Figure 2: I2C bus expansion board
The relay lies with the " +" connection to +5V of the AZ-Onboards, "-" goes to GND and the "S" connection is connected to GPIO15. GPIO13 of the AZ-EONboard leads via a 1.0kΩ resistor to the anode of the LED (longer connection), the cathode lies at GND.
Figure 3: Connection of relays and LEDs
The hardware
The usual board with the controller, here an ESP8266, is replaced by the AZ onboard. To the hardware list from the first episode have joined a relay, two resistors and two LEDs. The sensor boards are already included in the kit.
1 | AZ-EONboard Developmentboard including Extensionboards SHT30, BH1750 & SGP30 |
---|---|
1 | 0.91 inch OLED I2C Display 128 x 32 pixels |
1 | 1-relay 5V KY-019 module high-level trigger |
1 | LED green, for example LED light -emitting assortment kit, 350 pieces, 3mm & 5mm, 5 colors - 1x seT |
1 | LED white |
1 | Resistance 1.0kΩ for example Resistance resistor kit 525 pieces resistance range, 0 ohm -1m ohm |
1 | Resistance 150Ω |
1 | Mini Breadboard 400 PIN with 4 current rails |
1 | Jumper Wire cable 3 x 40 pcs. 20 cm M2M / F2M / F2F each |
optional | Logic Analyzer |
The software
For flashes and the programming of the controller:
Thonny or
For the first test of the AZ-Onoboard:
Terminal program Putty
Signal tracking:
Used firmware for an ESP32:
Used firmware for an ESP8266:
The micropython programs for the project:
timeout.py: Non-blocking software timer
oled.py: OLED-API
SSD1306.PY: OLED hardware driver
bh1750.py: Hardware driver module
bh1750_test.py: Demo program
bh1750_kal.py: Program for calibrating the Lux values
sht30.py: Hardware driver module
sht30_test.py: Demo program
sgp30.py: Hardware driver module
sgp30_s.py: slimmed down hardware driver module for ESP8266
sgp30_test.py: Demo program
sensortest.py: Demo program
gadget.py: Demo program
Micropython - Language - Modules and Programs
To install Thonny you will find one here detailed instructions (English version). There is also a description of how that Micropython firmware (As of 18.06.2022) on the ESP chip burned becomes. Like you that Raspberry Pi Pico get ready for use, you will find here.
Micropython is an interpreter language. The main difference to the Arduino IDE, where you always flash entire programs, is that you only have to flash the Micropython firmware once on the ESP32 so that the controller understands micropython instructions. You can use Thonny, µpycraft or ESPTOOL.PY. For Thonny I have the process here described.
As soon as the firmware has flashed, you can easily talk to your controller in a dialogue, test individual commands and see the answer immediately without having to compile and transmit an entire program beforehand. That is exactly what bothers me on the Arduino IDE. You simply save an enormous time if you can check simple tests of the syntax and hardware to trying out and refining functions and entire program parts via the command line before knitting a program from it. For this purpose, I always like to create small test programs. As a kind of macro, they summarize recurring commands. Whole applications then develop from such program fragments.
Autostart
If the program is to start autonomously by switching on the controller, copy the program text into a newly created blank tile. Save this file at Main.py in WorkSpace and upload it to the ESP chip. The program starts automatically the next time the reset or switching on.
Test programs
Programs from the current editor window in the Thonny-IDE are started manually via the F5 button. This can be done faster than the mouse click on the start button, or via the menu run. Only the modules used in the program must be in the flash of the ESP32.
In between, Arduino id again?
Should you later use the controller together with the Arduino IDE, just flash the program in the usual way. However, the ESP32/ESP8266 then forgot that it has ever spoken Micropython. Conversely, any espressif chip that contains a compiled program from the Arduino IDE or AT-Firmware or Lua or ... can be easily provided with the micropython firmware. The process is always like here described.
The preparation of the AZ onboard
The board comes with a finished programmed ESP8266. The program demonstrates the possibilities of the three sensors. Figure 4 shows the edition of the program with the terminal program Putty.
Figure 4: Edition with the demosoftware
But we want to write and let our own program in Micropython. The first step to the goal is that we burn a corresponding micropython core on the ESP8266 that overwrites the demo program. So first load them Firmware down and then follow of these instructions. The ESP8266 then reports in the Terminal of Thonny:
XXXXXXXXXX
Micropython V1.23.0 on 2024-06-02; ESP modules (1m) with ESP8266
Type "Help ()" for more information.
>>>
The SGP30 module
Like the other two sensors, the SGP30 is also controlled and queried by a set of commands. In the BH1750, the bytes were, at the SHT30 it was Words, i.e. 16-bit values. However, the data to be transmitted had the same format. After all, we had to take different times of change in these sensors, but which could be derived from the respective operating mode by calculating or from tables.
With the SGP30 there are also two-byte command discussions, but the feedback on the chip can be 0, 3, 6 or even 9 bytes long. In addition, there are also different delays until the data is available. If you don't want to write your own routine for every command, you have to trick.
We also find two other things that we had already used in the other sensors in this module. This is an exception class that is tailored to the SGP30 class and we also meet the test sum calculation again. For the ESP8266 I also have the module sgp30.py have to shrink healthy because the program for the three sensors was otherwise not to get to run - lack of memory. So here comes the slimmed down version sgp30_s.py.
The SGPerror class
The Exception class SGperror is similar to the construction of the Shterror class from the module sht30.pywhere this was also discussed in detail. I no longer go into this.
In the introduction of the SGP30 class, the import business. pack() from the module struct will help us to change the type. To calculate the absolute moisture of the air in g/l, we need the exponential function ex = exp ().
XXXXXXXXXX
From machine import Pin
From time import Sleep_ms
From sys import exit
From struct import pack
From math import Exp
Then follow a few constants, the device address of the SGP30 is 0x58 = 88 and the polynomial for CRC calculation is 0x131. Care, there is an error in the data sheet, There it says 0x31, that's curd! The start byte for CRC calculation is 0xff and the chip test always delivers the same result 0xD400.
XXXXXXXXXX
class SGP30():
Sgphwad = const(0x58)
Sgppoly = const(0x131) # !!! Attention: error in the data sheet !!!
# Page 12; Table 13; Line 5
Sgpcrcstart = const(0xff)
Sgptestresult = const(0xD400)
The trick on the commands is simple. Instead of my own routine for every command, I use a bytes object with four elements, which I will break open within the transmission routine. The first two bytes represent the command word that the third byte mentions the number to be received by bytes including the test amount and the fourth byte encodes the waiting time until the data is provided in milliseconds.
XXXXXXXXXX
# Command Word, Response Bytes, Delay in MS
SGPCMD_Getserial=b '\ x36 \ x82 \ x09 \ x02'
Sgpcmd_iaq_init=b '\ x20 \ x03 \ x00 \ x0a'
Sgpcmd_measure_iaq=b '\ x20 \ x08 \ x06 \ x0c'
Sgpcmd_get_iaq_baseline=B '\ x20 \ x15 \ x06 \ x0a'
Sgpcmd_set_iaq_baseline=B '\ X20 \ X1E \ x00 \ x0a'
Sgpcmd_set_absolute_humidity=b '\ x20 \ x61 \ x00 \ x0a'
Sgpcmd_measure_test=b '\ x20 \ x32 \ x03 \ xdc'
Sgpcmd_get_featpe_set=B '\ x20 \ x2f \ x03 \ x0a'
Sgpcmd_measure_raw=B '\ x20 \ x50 \ x06 \ x19'
Sgpcmd_get_tvoc_inceptive_baseline=b '\ x20 \ xb3 \ x03 \ x0a'
Sgpcmd_set_tvoc_baseline=B '\ x20 \ x77 \ x00 \ x0a'
The constructor takes a number of arguments. An I2C bus object is mandatory, optionally, different values for the hardware address, as well as for the parameters test and iaq_init be specified.
We pass the reference to the I2C bus object to the corresponding instance attribute and check whether the hardware address is included in the list I2C.Scan() delivers. If the SGP30 is not found, we will throw a SGPboarder Roror Exception. Otherwise we will remember the hardware address. We prove the absolute humidity with 13,355 g/l. This value should be cyclically from the room temperature and the relative humidity (measured with SHT30) using the method rel2abshumidity() to which we will come later.
XXXXXXXXXX
def __init__(
self, I2C,
ADR=Sgphwad,
test=False,
iaq_init=True):
self.I2C = I2C
IF ADR need in self.I2C.scan():
raise Sgperror(SGPboarderror)
self.hwad = ADR
self.abshum = 13.355
The serial number is read, as is the version of the feature set. If necessary, we collect a test measurement, the result of which must be 0xD400. We put the data received in Replica In conclusion, we initiate the IAQ measurement.
XXXXXXXXXX
self.serial = self.readerial()
self.feature set = self.feteatureset()
IF test:
IF Sgptestresult != self.Measure_test():
raise Sgperror(Sgpchiperror)
print(
"SGP30 initialized @ {} \ n".format(hex(self.hwad)),
"Serial id: {} \ n".format(self.serial),
"Feature Set: {} \ n".format(self.feature set),
"Algoritmus Init: {}".format(iaq_init))
IF iaq_init:
self.iaqinit()
crctest() calculates the test sum of the received data bytes and delivers True Back if this matches the SGP30 test sum. The chip always sends two data bytes followed by the test sum. The three bytes are on the parameter blobb Give over and first separated in the date of the date and the cheek. The for loops lead the CRC (Cyclic Redundancy Check) through.
XXXXXXXXXX
def crctest(self,blobb): # 6.6 Data = 2 bytes + CRC-byte
start = Sgpcrcstart
for h in blobb[:-1]:
start ^= h
for I in range(8):
IF start & 0x80:
start = (start << 1) ^ Sgppoly
Else:
start <<= 1
return start == blobb[-1]
Because we also want to send data to the SGP30 with a CRC-byte, we need a routine that calculates the test sum. We hand over the two data bytes and get the CRC-byte back.
XXXXXXXXXX
def CREATECRC(self,blobb): # 6.6 Data = 2 bytes
start=Sgpcrcstart
for h in blobb:
start ^= h
for I in range(8):
IF start & 0x80:
start = (start << 1) ^ Sgppoly
Else:
start <<= 1
return start & 0xff # CRC-byte
The serial number of the chip gets the routine readerial(). We get the numerical value by pushing and or taking the three received bytes. The MSB is the first byte, so it is in Words[0] and so on. The output format is hexadecimal.
XXXXXXXXXX
def reader(self): # 6.5
Words=self.Commandriderresult(SGP30.SGPCMD_Getserial)
serial=(((Words[0] << 16)| Words[1]) << 16) | Words[2]
return hex(serial)
The routines for sending comments and receiving the data are here in the method Commandriderresult() summarized. We already know the reason for this, the different length of the data to be received and the deviating waiting times. In addition to the command word, further data can be sent to the SGP30 instead of receiving them. In this case the parameter takes param this as Bytes sequence.
XXXXXXXXXX
def Commandriderresult(self,command,param=None): # 6.3
CMD=command[:2]
IF param is need None:
CMD += param
length=command[2]
delay=command[3]
The command word is in the first two bytes of command. If param not None contains, it contains data bytes that must be attached to the command word. We also filter out the number of bytes to be received and the waiting time. Then we write the command and wait delay MS. If now length = 0, no bytes are expected from the SGP30 and we're done.
Otherwise we will pick up the corresponding number of bytes and create an empty list for the data words received.
XXXXXXXXXX
self.I2C.writeto(self.hwad, CMD)
Sleep_ms(delay)
IF length == 0:
return None
blobb=self.I2C.readfom(self.hwad,length)
Words=[]
for I in range(length//3):
IF self.crctest(blobb[3*I:3*(I+1)]):
Words.append((blobb[3*I] << 8) | blobb[3*I+1])
Else:
raise Sgperror(Sgpcrcerror)
return Words
Because two data bytes and one CRC-byte are read per data word, come length // 3 packages. Each package is subjected to the CRC test. The index I runs from 0 to a maximum of 2 and addresses the slices blobb[0:3], blobb[3: 6] and blobb[6: 9]. If the CRC test was positive, we screw the data word together and add it to the list Words One that we return.
The following six methods now simply use the method Commandriderresult() and back to the command image sequence.
XXXXXXXXXX
def feteatureset(self):
return self.Commandriderresult(SGP30.Sgpcmd_get_featpe_set)[0]
def measuret test(self):
return self.Commandriderresult(SGP30.Sgpcmd_measure_test)[0]
def iaqinit(self):
self.Commandriderresult(SGP30.Sgpcmd_iaq_init)
def Measureiaq(self):
return self.Commandriderresult(SGP30.Sgpcmd_measure_iaq)
def trawval(self):
return self.Commandriderresult(SGP30.Sgpcmd_measure_raw)
def gettvoceptive basin(self):
return self.Commandriderresult(SGP30.Sgpcmd_get_tvoc_inceptive_baseline)
For a reset of the SGP30, General Call Address 0 is sent together with the byte 0x06. All chips that support this feature are reset. This also includes the SHT30.
XXXXXXXXXX
def soft reset(self):
self.I2C.writeto(0, B '\ x06')
The SGP30 needs the value of the absolute humidity in grams of water per liter of air for its correct function. With the method set tabsoluteehumidity() The value is sent to the SGP30 in 8.8 fixed point format. We talk about the format even more precisely later. The value consists of a data word (h) that we use by using the method pack() disassemble into two bytes, order Big Endian (>), i.e. MSB first. The method CREATECRC() provides a (1) byte that has to be converted into a bytes object before it blobb can be attached. big stands for Big Endian, which is of no importance in this context, but must be specified because of the syntax. We send the three bytes to the SGP30.
XXXXXXXXXX
def set tabsoluteehumidity(h): # Abshum Type 8.8
blobb = pack("> H",h)
blobb += self.CREATECRC(blobb).to_bytes(1,"Big")
self.Commandriderresult(SGP30.Sgpcmd_set_absolute_humidity,blobb)
The formula for calculating the absolute moisture is specified in the data sheet. With the help of the SHT30, the input values for temperature and rel. Moisture can be determined.
XXXXXXXXXX
def rel2abshumidity(self, T, rhinoceros): # 6.3 - P 10
self.abshum=216.7*((rhinoceros/100*6.112*Exp(17.62*T/(243.12+T))) (273.15+T))
abshum88=(intimately(self.abshum) << 8 ) | intimately((self.abshum % 1) * 256)
return self.abshum88
In abshum the decimal value is stored, as it would come out on a calculator. The SGP30 wants a different format, fixed comma 8.8. This means that the 8 bit of the MSB contain the integer share and the second 8 bits the counter of a break, with the denominator 256 and the value of the second -scale.
Be abshum = 13.355, then 13 = 0x0d is the integer share. We get the decimal abshum % 1, i.e. as a resort of 13.355: 1 = 0.355 or 355 thousandths. We receive the meter of the break with the denominator 256 by multiplying 0.355 with 256, which is 90.88. 90/256 is about 0.355 and 90 = 0x5a is the second byte. Composed, 0x0d5a comes out and is returned. The method wants such a value set tabsoluteehumidity() as an argument.
We can use the decimal value of the absolute moisture abshumidity Call up. The decorator @property allows the call as with a variable, the brackets are eliminated when the call is called.
XXXXXXXXXX
def abshumidity(self):
return self.abshum
XXXXXXXXXX
>>> SGP.abshumidity
13.355
The query of the measured values for CO2EQU in PPM (parts per million) and tvoc in ppb (parts per trillion (billion)) works in a similar way. The method provides these values Measureiaq() in a list that we access through indices 0 and 1.
XXXXXXXXXX
def Co2e(self):
return self.Measureiaq()[0]
def tvoc(self):
return self.Measureiaq()[1]
There is still no method to convert the decimal value of the moisture into 8.8 format.
XXXXXXXXXX
def abshum2fix88(self, h=None): # Dec to 8.8 Fixpoint
IF h is None:
h=self.abshum
Else:
self.abshum=h
abshum88=(intimately(h) << 8 ) | intimately((h % 1) * 256)
return abshum88
If no argument is specified when calling, then we use the value in abshum. If we have calculated a value manually, we can hand it over and to abshum pass on. We calculate the two fixed comma bytes and give back the 8.8 data word.
SGP30 test run
With the program sgp30_test.py we can test the new module. Define import business, bus object and hand over to the constructors of the SGP30 and OLED class.
XXXXXXXXXX
From machine import Pin,Softi2c
From SGP30_S import SGP30
From time import Sleep_ms
From OLED import OLED
From sys import exit
I2C=Softi2c(Pin(5),Pin(4),freq=100000)
SGP=SGP30(I2C)
D=OLED(I2C,Heightw=32)
AF88=SGP.abshum2fix88()
SGP.set tabsoluteehumidity(AF88)
while 1:
print("\ nco2eq; tvoc",SGP.Measureiaq())
D.clearall(False)
D.writer("Air quality",2,0,False)
D.writer("Co2e {:> 5} ppm".format(SGP.Co2e),0,1,False)
D.writer("Tvoc {:> 5} ppb".format(SGP.tvoc),0,2)
Sleep_ms(300)
We use the value for the absolute moisture presented in the constructor, switch it to 8.8 format and send it to the SGP30. In the While loop, we ask the measured values and issue them in Repliad and in the display.
Overall test of all sensors
The Test program It looks similar for all three sensors. Of course, all three classes have to be imported and objects have to be created with it.
XXXXXXXXXX
From machine import Pin,Softi2c
From SGP30_S import SGP30
From sht30_s import SHT3X
From BH1750 import BH1750
From OLED import OLED
From time import Sleep_ms, sleep
From sys import exit
From time-out import Time-out
I2C=Softi2c(Pin(5),Pin(4),freq=100000)
D=OLED(I2C,Heightw=32)
SHT=SHT3X(I2C)
SGP=SGP30(I2C)
bra=BH1750(I2C)
We define two PIN objects for the relay and the green LED.
XXXXXXXXXX
relay=Pin(15,Pin.OUT,value=0)
green=Pin(13,Pin.OUT,value=0)
Likewise the two variables for the monoflop, the delay switch. To the method Time-out() we'll get further down.
XXXXXXXXXX
triggered=False
expired = Time-out(0)
Finally, we initiate the work mode of the SHT30 and set the absolute moisture as above.
XXXXXXXXXX
MPS=3
SHT.StartPeriodic(MPS,0)
Sleep_ms(16)
AF88=SGP.abshum2fix88()
SGP.set tabsoluteehumidity(AF88)
In the loop we read the values and output them on the display.
XXXXXXXXXX
while 1:
SHT.readperiodic()
tempo,humble=SHT.Calcvalues()
Co2e,tvoc=SGP.Measureiaq()
lum=bra.luminance()
D.clearall(False)
D.writer("Lumi: {:> 5} Lux".format(lum),0,0,False)
D.writer("T: {: 0.1f}; H: {: 0.1f}".format(tempo,humble),0,1,
False)
D.writer("CO2 {:> 3}".format(Co2e),0,2,False)
D.writer("Tvoc {:> 3}".format(tvoc),8,2)
If the temperature is higher than 25 ° C, we switch on the relay. So that it does not flutter all the time when the temperature fluctuates slightly, we choose a slightly lower value for switching off and thus create a so -called Hysteresis, a switchover delay.
XXXXXXXXXX
IF tempo >= 25:
relay(1)
elif tempo <=24.5:
relay(0)
If the illuminance falls below 150 lux, the green LED is to be switched on and stayed for five seconds. We regulate this with the non -blocking timer using the method Time-out(). It gives a reference to the function encapsulated in it compare(), one Closure, back that we the identifier expired assign. The call from expired() delivers True Back when the 5 seconds are up. In the meantime, however, the While loop will continue to be carried out. triggered = True indicates that the LED is already switched on. The block may only be executed if the LED does not yet light up.
XXXXXXXXXX
IF lum < 150 and need triggered:
green(1)
expired = Time-out(5)
triggered = True
If the timer has expired and the LED, it must be switched off. triggered Let's put false again.
XXXXXXXXXX
IF expired() and triggered:
green(0)
triggered = False
Sleep_ms(SHT3X.Querydelay[MPS])
We are still waiting for the SHT30 measuring cycle to end and start the next round.
Lit and blow out the LED
As a gag, I had built a circuit with an LDR, a transistor and a light beam as a gag.
Figure 5: lamp gadget
If you hold a burning match between the light bulb and the LDR, the lamp starts. If you blow the lamp so that it swings to the side, the lamp goes out.
The arrangement can be set up as a digital variant with the AZ-EONboard and the BH1750. The incandescent lamp is replaced by a white LED and the LDR by the BH1750. The ESP8266 takes care of the implementation of the very simple tax program.
Figure 6: Lamp gadget with AZ-Onoboard, BH1750 and LED
We measure a few illuminated strengths in advance. For safety reasons, we do not use an open fire, but a flashlight.
Normal ambient light on the BH1750: 467
With the LED switched on at a distance of 5 cm: 3853
Flashlight at 20cm away: 8310
We are based on these values in program When specifying the limit values.
XXXXXXXXXX
From machine import Pin,Softi2c
From BH1750 import BH1750
From time import Sleep_ms
I2C=Softi2c(Pin(5),Pin(4),freq=100000)
bra=BH1750(I2C)
white=Pin(14,Pin.OUT,value=1)
while 1:
lum = bra.luminance()
IF lum > 3000:
white(0)
IF lum < 1000:
white(1)
Sleep_ms(120)
After the imports we instantiate an I2C bus object again and assign it to the BH1750 constructor. We connect the cathode of the LED to GPIO14 via a resistance, the anode we put on the +5V connection of the AZ onboard. The LED is out when the output is logical 1. The voltage of GPIO14 is now 3.1V and is therefore absolutely in the green. If we put the output on logical 0, the LED lights up. A current of 10.8mA flows, also okay!
The BH1750 works in the Continuous Highthresolution fashion. It therefore needs 120ms break between two measurements. We pick up a value and check for the area.
The value of 3000 ensures that the LED starts with the flashlight when light lighting the BH1750 and remains in order to continue to illuminate the sensor with 3800 lux.
If the LED is deflected far enough from its direction to the BH1750, for example by blowing, the exposure value falls below 1000 and the LED is switched off.