Measuring Air Quali...
 
Notifications
Clear all

Measuring Air Quality with ESP32 & Arduino

23 Posts
6 Users
8 Reactions
2,747 Views
(@dronebot-workshop)
Workshop Guru Admin
Joined: 5 years ago
Posts: 1097
Topic starter  

Use an ESP32 or Arduino to measure the quality of the air you breathe! Today, we'll work with several air quality sensors.

Pollution is a problem that affects everyone, no matter where they live. Even if you reside in a rural area, you are still subject to many types of pollution, both outdoors and inside your home.

Today we will look at several air quality sensors that you can use with a microcontroller. We’ll test them out using both an ESP323 and an Arduino, and we’ll also compare the readings to a commercial air quality meter to see if there is any correlation between readings.

We’ll be taking a look at the following sensors:

- MQ Gas Sensors (various models).
- PMS5003 PM2.5 Particulate Matter Sensor.
- BME280 Temperature, Humidity & Air Pressure Sensor.
- BME680 Temperature, Humidity & Gas Sensor.
- AHT20 Precision Temperature & Humidity Sensor.
- CCS811 Air Quality Sensor.
- SGP30 Air Quality Sensor.
- SGP40 Air Quality Sensor.

We’ll see how they work and what parameters they can measure, and we’ll hook them up and run a demo.

Then we’ll put a bunch of sensors together on an ESP32 to make an environmental monitoring platform.

Here is the Table of Contents for today's video:

00:00 - Introduction
01:38 - Air Quality
03:36 - Look at sensors
05:12 - Sensor Calibration Issues
06:46 - MQ Sensors Intro
12:25 - MQ Sensors Library & Code
16:42 - MQ Sensors ESP32 Considerations
21:00 - PM2.5 Sensors
27:55 - Temperature & Humidity Sensors Intro
31:25 - BME280 Demo
33:28 - BME680 Demo
35:49 - AHT20 Demo
37:33 - Air Quality Sensors Intro
39:40 - CCS811 Demo
43:09 - SGP30 Demo
45:38 - SGP40 Demo
47:36 - ESP32 Multi-Sensor
58:45 - Conclusion

On a personal note, this project actually alerted me to several areas in my home that I need to improve the air circulation in. Hopefully, you will find it equally useful!

Bill

"Never trust a computer you can’t throw out a window." — Steve Wozniak


   
Quote
Ron
 Ron
(@zander)
Father of a miniature Wookie
Joined: 4 years ago
Posts: 7334
 

Is there any reason why the PICO could not be used to replace both the UNO and ESP32? Of course I assume an external 5V supply. Another viewer suggested the ADS1115 could be used to resolve some level issues as well.

First computer 1959. Retired from my own computer company 2004.
Hardware - Expert in 1401, and 360, fairly knowledge in PC plus numerous MPU's and MCU's
Major Languages - Machine language, 360 Macro Assembler, Intel Assembler, PL/I and PL1, Pascal, Basic, C plus numerous job control and scripting languages.
My personal scorecard is now 1 PC hardware fix (circa 1982), 1 open source fix (at age 82), and 2 zero day bugs in a major OS.


   
ReplyQuote
(@dronebot-workshop)
Workshop Guru Admin
Joined: 5 years ago
Posts: 1097
Topic starter  

@zander A Pico would work just fine, as would pretty well any microcontroller.

😎

Bill

"Never trust a computer you can’t throw out a window." — Steve Wozniak


   
Ron reacted
ReplyQuote
Ron
 Ron
(@zander)
Father of a miniature Wookie
Joined: 4 years ago
Posts: 7334
 

@dronebot-workshop Great, a PICOW with the AQ data on a web page seems like an obvious extension of the project.

First computer 1959. Retired from my own computer company 2004.
Hardware - Expert in 1401, and 360, fairly knowledge in PC plus numerous MPU's and MCU's
Major Languages - Machine language, 360 Macro Assembler, Intel Assembler, PL/I and PL1, Pascal, Basic, C plus numerous job control and scripting languages.
My personal scorecard is now 1 PC hardware fix (circa 1982), 1 open source fix (at age 82), and 2 zero day bugs in a major OS.


   
ReplyQuote
(@bob-s)
Member
Joined: 4 years ago
Posts: 8
 

Are there any units for the Standard and Environmental PM values?

e.g. milligrams per cubic meter or something?

What is the meaning of or difference between Standard and Environmental?

I noticed that at low levels the values are the same for Standard and Environmental...is there a reason for that?

Thanks, Bob

image

   
ReplyQuote
Ron
 Ron
(@zander)
Father of a miniature Wookie
Joined: 4 years ago
Posts: 7334
 

@bob-s If you look at Bill's sketch you can determine where the data comes from. I suspect that will lead to a library and looking at the library code might educate you. Otherwise try google.

First computer 1959. Retired from my own computer company 2004.
Hardware - Expert in 1401, and 360, fairly knowledge in PC plus numerous MPU's and MCU's
Major Languages - Machine language, 360 Macro Assembler, Intel Assembler, PL/I and PL1, Pascal, Basic, C plus numerous job control and scripting languages.
My personal scorecard is now 1 PC hardware fix (circa 1982), 1 open source fix (at age 82), and 2 zero day bugs in a major OS.


   
ReplyQuote
TBerryKev
(@tberrykev)
Member
Joined: 2 years ago
Posts: 16
 

@bob-s The units for the standard and environmental measurements are micrograms/cubic meter.

I haven't found anything I would consider conclusive regarding the difference between "standard" and "environmental" measurements but a comment by @guolivar in this discussion is consistent with how those terms are used in my industry where we work a lot with concentrations of various things in real gases.  If @guolivar is correct the "standard" measurement corrects the "environmental" measurement to a pressure and temperature standard (15 deg-C and 760mm Hg) which roughly corresponds to a cool day at sea level.

Actually performing this correction would require some measurement for ambient pressure and temperature within the PMS5003. I'm not aware that the PMS5003 has the sensors to do that so this whole hypothesis may be completely wrong. (Actually, now that I think about it,  air density could probably be backed out from the current draw by the fan).

Anyway, in my testing so far, my values for "Standard" and "Environmental" values are always nearly equal and I live near sea level and am using the unit indoors.  It would be interesting to hear if anyone is using the unit at a high altitude and seeing a greater difference in the values.

 

Postscript - After additional searching I found this explaination and the end of the datasheet for a similar (Seeed/Grove) sensor which appears to provide basically identical output:

Note The standard particulate matter mass concentration value refers to the mass concentration value obtained by density conversion of industrial metal particles as equivalent particles, and is suitable for use in industrial production workshops and the like. The concentration of particulate matter in the atmospheric environment is converted by the density of the main pollutants in the air as equivalent particles, and is suitable for ordinary indoor and outdoor atmospheric environments. So you can see that there are two sets of data above.

 

-- Kevin


   
ReplyQuote
(@bob-s)
Member
Joined: 4 years ago
Posts: 8
 

@tberrykev 

@zander

Thanks for the replies all.
Thanks for searching, (I searched too and found µg/m³ is appropriate) so I will use that.
However I did not find the difference between standard and environmental.
So based on what you found, Kevin I will use the environmental measurements and delete the standard measurements.
I did notice that only on high concentrations that values differed a bit.

Ron, I was unable to find any clarification in the library for the question about standard vs environmental.

Now I will try to find out what's going on with the Raw H2 and Raw Ethanol.
If I rub my hands with an alcohol based hand sanitizer and hold them near the SGP30, the TVOC and eCO2 go way up but the H2 and ethanol readings actually go down.
It could be the code...I was unable to get the SGP30 working from the code in the tutorial and had to change all occurrences of sgp30 to just sgp.
I think I got the code from the Adafruit website but I'm not sure so perhaps that's a problem.

This is a great tutorial/project.


   
ReplyQuote
Ron
 Ron
(@zander)
Father of a miniature Wookie
Joined: 4 years ago
Posts: 7334
 

@bob-s All I have been able to find so far is it is part of the data protocol for a whole family of sensors. I have not had any luck beyond that, but Kevin's @tberrykev observation sounds right. 760mmHG is the STANDARD, but although 15C isn't any STANDARD I recognize that doesn't mean it isn't correct. For trend analyses the difference is not important, simply stick with one or the other but I would favor ENV. 

First computer 1959. Retired from my own computer company 2004.
Hardware - Expert in 1401, and 360, fairly knowledge in PC plus numerous MPU's and MCU's
Major Languages - Machine language, 360 Macro Assembler, Intel Assembler, PL/I and PL1, Pascal, Basic, C plus numerous job control and scripting languages.
My personal scorecard is now 1 PC hardware fix (circa 1982), 1 open source fix (at age 82), and 2 zero day bugs in a major OS.


   
TBerryKev reacted
ReplyQuote
TBerryKev
(@tberrykev)
Member
Joined: 2 years ago
Posts: 16
 

Possible Issue with readPMSdata() function in PM5003 Code

This excellent video was posted at a very good time for me.  I've been planning to construct a sensor project for my workshop that will turn on some dust extraction when dust levels increase during woodworking and, when necessary, turn off my mini-split air conditioner until dust levels subside.

I ordered the PM5003 from Adafruit and put the project together on a breadboard.  Unfortunately after about a day-and-a-half the code stopped reporting new values from the sensor which presented itself as flatlining on the dashboard I had made on the Arduino IOT Cloud.

Screen Shot 2022 11 07 at 12.05.06 PM

After several more resets, I consistently got failures anywhere between a few hours of run time to a day-and-half of runtime.

At first, like Bill, I suspected my sensor, but after several days of testing and reviewing the PMS5003 datasheet, I zeroed in on what I'm pretty sure is an oversight in the code from the post on How2Electronics

In short the code from How2Electronics searches the serial buffer for "0x42" but according to the datasheet, readable data actually begins with 2 recognizable bytes--a "0x42" followed by a "0x4D."

As someone who has only been playing with Arduino for a month or so and as someone whose only coding experience is some Fortran coding 30 years ago, I'm eager for some second/third/etc. opinions.  I've never written a post like this before so I imagine it could be done much better--or at least more efficiently.  My gratitude to those who choose to wade through it.

To diagnose the problem I added/turned on the debugging from the readPMSdata() function in the code I got from Bill's post.

// debugging
Serial.print("Ox"); Serial.print(buffer[0], HEX); Serial.print(", ");
Serial.print("Ox"); Serial.print(buffer[1], HEX); Serial.print(", ");
Serial.println();
for (uint8_t i=2; i<32; i++) {
Serial.print("Ox"); Serial.print(buffer[i], HEX); Serial.print(", ");
}
Serial.println();

After several hours I got the usual failure with this resulting Serial output.  Note the appearance of "Checksum failure" about half-way down.

Ox42, Ox4D,
Ox0, Ox1C, Ox0, Ox6, Ox0, OxA, Ox0, OxB, Ox0, Ox6, Ox0, OxA, Ox0, OxB, Ox4, OxE6, Ox1, Ox75, Ox0, Ox44, Ox0, Ox6, Ox0, Ox2, Ox0, Ox0, Ox97, Ox0, Ox3, Ox24,
PM 1.0: 6 PM 2.5: 10 PM 10: 11
Ox42, Ox4D,
Ox0, Ox1C, Ox0, Ox6, Ox0, OxA, Ox0, OxB, Ox0, Ox6, Ox0, OxA, Ox0, OxB, Ox4, OxB9, Ox1, Ox68, Ox0, Ox4A, Ox0, Ox6, Ox0, Ox2, Ox0, Ox0, Ox97, Ox0, Ox2, OxF0,
PM 1.0: 6 PM 2.5: 10 PM 10: 11
Ox42, Ox0,
Ox2, Ox0, Ox0, Ox0, Ox0, Ox97, Ox0, Ox2, Ox75, Ox42, Ox4D, Ox0, Ox1C, Ox0, Ox8, Ox0, OxA, Ox0, OxA, Ox0, Ox8, Ox0, OxA, Ox0, OxA, Ox5, Ox3A, Ox1, Ox7D, Ox0,
Checksum failure
Ox42, Ox0,
Ox2, Ox0, Ox0, Ox0, Ox0, Ox97, Ox0, Ox2, Ox7B, Ox42, Ox4D, Ox0, Ox1C, Ox0, Ox8, Ox0, OxA, Ox0, OxA, Ox0, Ox8, Ox0, OxA, Ox0, OxA, Ox5, Ox3A, Ox1, Ox7D, Ox0,
Checksum failure
Ox42, Ox0,
Ox2, Ox0, Ox0, Ox0, Ox0, Ox97, Ox0, Ox2, Ox7B, Ox42, Ox4D, Ox0, Ox1C, Ox0, Ox7, Ox0, OxA, Ox0, OxA, Ox0, Ox7, Ox0, OxA, Ox0, OxA, Ox5, Ox1, Ox1, Ox6E, Ox0,
Checksum failure
Ox42, Ox0,
Ox2, Ox0, Ox0, Ox0, Ox0, Ox97, Ox0, Ox2, Ox31, Ox42, Ox4D, Ox0, Ox1C, Ox0, Ox7, Ox0, OxA, Ox0, OxA, Ox0, Ox7, Ox0, OxA, Ox0, OxA, Ox5, Ox1, Ox1, Ox6E, Ox0,
Checksum failure
Ox42, Ox0,
Ox2, Ox0, Ox0, Ox0, Ox0, Ox97, Ox0, Ox2, Ox31, Ox42, Ox4D, Ox0, Ox1C, Ox0, Ox7, Ox0, OxA, Ox0, OxA, Ox0, Ox7, Ox0, OxA, Ox0, OxA, Ox5, Ox1, Ox1, Ox6E, Ox0,
Checksum failure
Failed on s->available() < 32
Failed on s->available() < 32
Failed on s->available() < 32
Failed on s->available() < 32

In this output note that I output the first two bytes when "0x42" is identfied and normally the "0x42" is followed by "0x4D" until things go wrong, then the second byte is "0x0" which normally separates valid data arguments from the PMS5003.  It appears that occasionally the PMS5003 reports "0x42" as a data argument and those need to be differentiated from the start byte by the second byte of "0x4D."  Consequently a "Checksum Failure" is reported and then everything falls apart.  The error "Failed on s->available() < 32" continued for hours.

It is very interesting to note that in the output above you can see the "0x42, 0x4D" bytes in the rows that received the "Checksum Failure."  Apparently there was an "Ox42" slightly upstream of the actual start byte followed by "0x0" which was creating the problem.

It seems there are two issues here:

1. Failure to check the byte following "0x42" results in some premature positives on data reception.

2. The code can't always recover after a "Failed on s->available() < 32" error.

 

For now, I've focused on the first problem.

The code to peek the buffer and then read the values uses pointers within classes which is still over my head at this point.  I tried for some time to figure it all out and finally resorted to a solution which is less elegant but got me moving forward again.  Essentially my solution is as follows:

 

1) Let the existing code find the "0x42" and read that value into variable called "byte0."

2) Read the second byte from the serial buffer and put that into "byte1"

3) Check to see if "byte1" has a value of "0x4D" and if it doesn't go back to peeking the serial buffer.

4) If "byte1" has a value of "0x4D," place "0x42" and "0x4D" in the first two bytes of the "buffer[]" array and fill the remainder of the array with the existing code.

5) Continue the readPMSdata() function as originally written.

Even I can see inefficiencies with my approach, but I think I have confirmed the problem and have a functional workaround until my coding skills improve.  My first stable version of the code and the resulting debugging data follows.  As of now the code has run almost four days without issue.

boolean readPMSdata(Stream *s) {
if (! s->available()) {
Serial.println("Fail on if (! s->available())");
return false;
}

// Read a byte at a time until we get to the special '0x42' start-byte
if (s->peek() != 0x42) {
Serial.println("Not 0x42");
s->read();
return false;
}

// Stash the 0x42 for later in byte0
int byte0 = s->read();
Serial.print("First byte: 0x");
Serial.println(byte0, HEX);

// Read the next byte to make sure it is the special '0x4D' second start-byte
if (s->peek() != 0x4D) {
Serial.println("Misleading 0x42 Detected");
// If it isn't '0x4D' then throw it away
s->read();
return false;
}

// Stash the 0x4D for later in byte1
int byte1 = s->read();
Serial.print("Second byte: 0x");
Serial.println(byte1, HEX);

// Now read all 32 bytes (Reduced to 30 because of 2 bytes already read.)
if (s->available() < 30) {
Serial.println("Failed on s->available() < 32");
return false;
}


// Adding preBuffer[30] with which to assemble buffer[32] along with byte0 and byte1
uint8_t preBuffer[30];
uint8_t buffer[32];
uint16_t sum = 0;
s->readBytes(preBuffer, 30);

// Transfer byte0 and byte1 to buffer.
buffer[0] = byte0;
buffer[1] = byte1;

// Transfer preBuffer[0:29] to buffer[2:31]
for(int iByte = 2; iByte<32; iByte++) {
buffer[iByte] = preBuffer[iByte-2];
}

// get checksum ready
for (uint8_t i = 0; i < 30; i++) {
sum += buffer[i];
}

// debugging
Serial.print("Ox"); Serial.print(buffer[0], HEX); Serial.print(", ");
Serial.print("Ox"); Serial.print(buffer[1], HEX); Serial.print(", ");
Serial.println();
for (uint8_t i=2; i<32; i++) {
Serial.print("Ox"); Serial.print(buffer[i], HEX); Serial.print(", ");
}
Serial.println();


// The data comes in endian'd, this solves it so it works on all platforms
uint16_t buffer_u16[15];
for (uint8_t i = 0; i < 15; i++) {
buffer_u16[i] = buffer[2 + i * 2 + 1];
buffer_u16[i] += (buffer[2 + i * 2] << 8);
}

// put it into a nice struct :)
memcpy((void *)&data, (void *)buffer_u16, 30);

if (sum != data.checksum) {
Serial.println("Checksum failure");
return false;
}
// success!
return true;
}

The debugging is verbose. 

  1. Every time a byte is read that isn't a start byte, the statement "Not 0x42" is printed. 
  2. If a potential start byte is found the statement "First byte: 0x42" is printed
  3. If a "0x42" is detected and that is not followed by "0x4D" the statement "Misleading 0x42 Detected" is printed.
  4. If the second start byte is found the statement "Second byte: 0x42" is printed followed by the remaining contents of the "buffer[]" on the next line and the PM values follow.

A snippet of the output from the stable code follows.  Note about halfway down a "Misleading 0x42 Detected" is reported, but the program continues without issue.

First byte: 0x42
Second byte: 0x4D
Ox42, Ox4D,
Ox0, Ox1C, Ox0, Ox6, Ox0, Ox9, Ox0, OxB, Ox0, Ox6, Ox0, Ox9, Ox0, OxB, Ox4, Ox47, Ox1, Ox43, Ox0, Ox3A, Ox0, OxA, Ox0, Ox2, Ox0, Ox0, Ox97, Ox0, Ox2, Ox4B,
PM 1.0: 6 PM 2.5: 9 PM 10: 11
First byte: 0x42
Second byte: 0x4D
Ox42, Ox4D,
Ox0, Ox1C, Ox0, Ox6, Ox0, Ox9, Ox0, OxB, Ox0, Ox6, Ox0, Ox9, Ox0, OxB, Ox4, Ox47, Ox1, Ox43, Ox0, Ox3A, Ox0, OxA, Ox0, Ox2, Ox0, Ox0, Ox97, Ox0, Ox2, Ox4B,
PM 1.0: 6 PM 2.5: 9 PM 10: 11
First byte: 0x42
Second byte: 0x4D
Ox42, Ox4D,
Ox0, Ox1C, Ox0, Ox6, Ox0, Ox9, Ox0, OxB, Ox0, Ox6, Ox0, Ox9, Ox0, OxB, Ox4, Ox47, Ox1, Ox43, Ox0, Ox3A, Ox0, OxA, Ox0, Ox2, Ox0, Ox0, Ox97, Ox0, Ox2, Ox4B,
PM 1.0: 6 PM 2.5: 9 PM 10: 11
Not 0x42
Not 0x42
Not 0x42
Not 0x42
Not 0x42
Not 0x42
Not 0x42
Not 0x42
First byte: 0x42
Misleading 0x42 Detected
Not 0x42
Not 0x42
Not 0x42
Not 0x42
Not 0x42
Not 0x42
Not 0x42
Not 0x42
Not 0x42
First byte: 0x42
Second byte: 0x4D
Ox42, Ox4D,
Ox0, Ox1C, Ox0, Ox6, Ox0, OxA, Ox0, OxC, Ox0, Ox6, Ox0, OxA, Ox0, OxC, Ox4, Ox8F, Ox1, Ox60, Ox0, Ox42, Ox0, OxA, Ox0, Ox2, Ox0, Ox0, Ox97, Ox0, Ox2, OxBC,
PM 1.0: 6 PM 2.5: 10 PM 10: 12
First byte: 0x42
Second byte: 0x4D
Ox42, Ox4D,
Ox0, Ox1C, Ox0, Ox6, Ox0, Ox9, Ox0, OxB, Ox0, Ox6, Ox0, Ox9, Ox0, OxB, Ox4, Ox62, Ox1, Ox55, Ox0, Ox3A, Ox0, OxA, Ox0, Ox2, Ox0, Ox0, Ox97, Ox0, Ox2, Ox78,
PM 1.0: 6 PM 2.5: 9 PM 10: 11
Not 0x42

 

Thanks in advance for any input, confirmations or suggestions. 

 

 

-- Kevin


   
ReplyQuote
TBerryKev
(@tberrykev)
Member
Joined: 2 years ago
Posts: 16
 

@zander The 15 deg-C came from a link in the post I referenced by @guolivar with the following remark:

The "U.S. Standard Atmosphere 1976" is an atmospheric model of how the pressure, temperature, density, and viscosity of the Earth's atmosphere changes with altitude. It is defined as having a temperature of 288.15 K (15 oC, 59 oF) at the sea level 0 km geo-potential height and 101325 Pa (1013.25 hPa, 1013.25 mbar, 760 mm Hg, 29.92 in Hg).

I don't have a lot of confidence in whether this addresses the terminology for the PMS5003 but shared it because there is so little else to go on.

-- Kevin


   
ReplyQuote
Ron
 Ron
(@zander)
Father of a miniature Wookie
Joined: 4 years ago
Posts: 7334
 

@tberrykev Yes I know. I have never heard of that 'standard' before though. As I said, for trend analyses it doesn't matter which you use although I would personally favour the 'environmental' data as it is likely more accurate.

First computer 1959. Retired from my own computer company 2004.
Hardware - Expert in 1401, and 360, fairly knowledge in PC plus numerous MPU's and MCU's
Major Languages - Machine language, 360 Macro Assembler, Intel Assembler, PL/I and PL1, Pascal, Basic, C plus numerous job control and scripting languages.
My personal scorecard is now 1 PC hardware fix (circa 1982), 1 open source fix (at age 82), and 2 zero day bugs in a major OS.


   
TBerryKev reacted
ReplyQuote
Ron
 Ron
(@zander)
Father of a miniature Wookie
Joined: 4 years ago
Posts: 7334
 

@tberrykev I think your analysis is correct, but instead of throwing away data, just look for the 2 byte separator of 0x42 0x4D. Any 0x42 without a following 0x4D is data unless I am very mistaken. 

First computer 1959. Retired from my own computer company 2004.
Hardware - Expert in 1401, and 360, fairly knowledge in PC plus numerous MPU's and MCU's
Major Languages - Machine language, 360 Macro Assembler, Intel Assembler, PL/I and PL1, Pascal, Basic, C plus numerous job control and scripting languages.
My personal scorecard is now 1 PC hardware fix (circa 1982), 1 open source fix (at age 82), and 2 zero day bugs in a major OS.


   
ReplyQuote
(@davee)
Member
Joined: 3 years ago
Posts: 1765
 

Hi @bob-s ,

Now I will try to find out what's going on with the Raw H2 and Raw Ethanol.
If I rub my hands with an alcohol based hand sanitizer and hold them near the SGP30, the TVOC and eCO2 go way up but the H2 and ethanol readings actually go down.

At a guess ... Your hand sanitizer may be based on isopropyl alcohol (IPA) ... not ethanol.  Try giving the ethanol sensor a 'spirits' drink like brandy or vodka, to sniff -- that definitely should be ethanol!

You may want to look up the specification of the sensors .. Although a sensor may be 'optimised' for a particular compound, it is quite dificult to make a sensor which is not affected by other substances that are chemically 'related'. 

Best wishes, Dave


   
ReplyQuote
TBerryKev
(@tberrykev)
Member
Joined: 2 years ago
Posts: 16
 
Posted by: @zander

@tberrykev I think your analysis is correct, but instead of throwing away data, just look for the 2 byte separator of 0x42 0x4D. Any 0x42 without a following 0x4D is data unless I am very mistaken. 

Thanks, Ron, 

That’s what I wanted to do, but I didn’t know how to peek past the first byte and it seemed to me that when you read a byte it’s removed from the buffer. 

If anyone can recommend the actual code adjustments, I’d love to learn how to do it.   

 

-- Kevin


   
ReplyQuote
Page 1 / 2