Skip to content

Commit 277d723

Browse files
committed
Add MQ135 gas sensor
1 parent ec138a6 commit 277d723

File tree

2 files changed

+200
-0
lines changed

2 files changed

+200
-0
lines changed

pslab/external/gas_sensor.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""Gas sensors can be used to measure the concentration of certain gases."""
2+
3+
from typing import Callable, Union
4+
5+
from pslab import Multimeter
6+
from pslab.serial_handler import SerialHandler
7+
8+
9+
class MQ135:
10+
"""MQ135 is a cheap gas sensor that can detect several harmful gases.
11+
12+
The MQ135 is most suitable for detecting flammable gases, but can also be
13+
used to measure CO2.
14+
15+
Parameters
16+
----------
17+
gas : {CO2, CO, EtOH, NH3, Tol, Ace}
18+
The gas to be detected:
19+
CO2: Carbon dioxide
20+
CO: Carbon monoxide
21+
EtOH: Ethanol
22+
NH3: Ammonia
23+
Tol: Toluene
24+
Ace: Acetone
25+
r_load : float
26+
Load resistance in ohm.
27+
device : :class:`SerialHandler`, optional
28+
Serial connection to PSLab device. If not provided, a new one will be
29+
created.
30+
channel : str, optional
31+
Analog input on which to monitor the sensor output voltage. The default
32+
value is CH1. Be aware that the sensor output voltage can be as high
33+
as 5 V, depending on load resistance and gas concentration.
34+
r0 : float, optional
35+
The sensor resistance when exposed to 100 ppm NH3 at 20 degC and 65%
36+
RH. Varies between individual sensors. Optional, but gas concentration
37+
cannot be measured unless R0 is known. If R0 is not known,
38+
:meth:`measure_r0` to find it.
39+
temperature : float or Callable, optional
40+
Ambient temperature in degC. The default value is 20. A callback can
41+
be provided in place of a fixed value.
42+
humidity : float or Callable, optional
43+
Relative humidity between 0 and 1. The default value is 0.65. A
44+
callback can be provided in place of a fixed value.
45+
"""
46+
47+
# Parameters manually extracted from data sheet.
48+
# ppm = A * (Rs/R0) ^ B
49+
_PARAMS = {
50+
"CO2": [109, -2.88],
51+
"CO": [583, -3.93],
52+
"EtOH": [76.4, -3.18],
53+
"NH3": [102, -2.49],
54+
"Tol": [44.6, -3.45],
55+
"Ace": [33.9, -3.42],
56+
}
57+
58+
# Assuming second degree temperature dependence and linear humidity dependence.
59+
_TEMPERATURE_CORRECTION = [3.28e-4, -2.55e-2, 1.38]
60+
_HUMIDITY_CORRECTION = -2.24e-1
61+
62+
def __init__(
63+
self,
64+
gas: str,
65+
r_load: float,
66+
device: SerialHandler = None,
67+
channel: str = "CH1",
68+
r0: float = None,
69+
temperature: Union[float, Callable] = 20,
70+
humidity: Union[float, Callable] = 0.65,
71+
):
72+
self._multimeter = Multimeter(device)
73+
self._params = self._PARAMS[gas]
74+
self.channel = channel
75+
self.r_load = r_load
76+
self.r0 = r0
77+
self.vcc = 5
78+
79+
if isinstance(temperature, Callable):
80+
self._temperature = temperature
81+
else:
82+
83+
def _temperature():
84+
return temperature
85+
86+
self._temperature = _temperature
87+
88+
if isinstance(humidity, Callable):
89+
self._humidity = humidity
90+
else:
91+
92+
def _humidity():
93+
return humidity
94+
95+
self._humidity = _humidity
96+
97+
@property
98+
def _voltage(self):
99+
return self._multimeter.measure_voltage(self.channel)
100+
101+
@property
102+
def _correction(self):
103+
"""Correct sensor resistance for temperature and humidity.
104+
105+
Coefficients are averages of curves fitted to temperature data for 33%
106+
and 85% relative humidity extracted manually from the data sheet.
107+
Humidity dependence is assumed to be linear, and is centered on 65% RH.
108+
"""
109+
t = self._temperature()
110+
h = self._humidity()
111+
a, b, c, d = *self._TEMPERATURE_CORRECTION, self._HUMIDITY_CORRECTION
112+
return a * t ** 2 + b * t + c + d * (h - 0.65)
113+
114+
@property
115+
def _sensor_resistance(self):
116+
return (
117+
(self.vcc / self._voltage - 1) * self.r_load / self._correction
118+
)
119+
120+
def measure_concentration(self):
121+
"""Measure the concentration of the configured gas.
122+
123+
Returns
124+
-------
125+
concentration : float
126+
Gas concentration in ppm.
127+
"""
128+
try:
129+
return self._params[0] * (self._sensor_resistance / self.r0) ** self._params[1]
130+
except TypeError:
131+
raise TypeError("r0 is not set.")
132+
133+
def measure_r0(self, gas_concentration: float):
134+
"""Determine sensor resistance at 100 ppm NH3 in otherwise clean air.
135+
136+
For best results, monitor R0 over several hours and use the average
137+
value.
138+
139+
The sensor resistance at 100 ppm NH3 (R0) is used as a reference
140+
against which the present sensor resistance must be compared in order
141+
to calculate gas concentration.
142+
143+
R0 can be determined by calibrating the sensor at any known gas
144+
concentration.
145+
146+
Parameters
147+
----------
148+
gas_concentration : float
149+
A known concentration of the configured gas in ppm.
150+
151+
Returns
152+
-------
153+
r0 : float
154+
The sensor resistance at 100 ppm NH3 in ohm.
155+
"""
156+
return self._sensor_resistance * (gas_concentration / self._params[0]) ** (
157+
1 / -self._params[1]
158+
)

tests/test_mq135.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pytest
2+
3+
from pslab.external.gas_sensor import MQ135
4+
5+
R_LOAD = 22e3
6+
R0 = 50e3
7+
VCC = 5
8+
VOUT = 2
9+
A, B, C, D = *MQ135._TEMPERATURE_CORRECTION, MQ135._HUMIDITY_CORRECTION
10+
E, F = MQ135._PARAMS["CO2"]
11+
STANDARD_CORRECTION = A * 20 ** 2 + B * 20 + C + D * (0.65 - 0.65)
12+
EXPECTED_SENSOR_RESISTANCE = (VCC / VOUT - 1) * R_LOAD / STANDARD_CORRECTION
13+
CALIBRATION_CONCENTRATION = E * (EXPECTED_SENSOR_RESISTANCE / R0) ** F
14+
15+
16+
@pytest.fixture
17+
def mq135(mocker):
18+
mock = mocker.patch("pslab.external.gas_sensor.Multimeter")
19+
mock().measure_voltage.return_value = VOUT
20+
return MQ135("CO2", R_LOAD)
21+
22+
23+
def test_correction(mq135):
24+
assert mq135._correction == STANDARD_CORRECTION
25+
26+
27+
def test_sensor_resistance(mq135):
28+
assert mq135._sensor_resistance == EXPECTED_SENSOR_RESISTANCE
29+
30+
31+
def test_measure_concentration(mq135):
32+
mq135.r0 = R0
33+
assert mq135.measure_concentration() == E * (EXPECTED_SENSOR_RESISTANCE / R0) ** F
34+
35+
36+
def test_measure_concentration_r0_unset(mq135):
37+
with pytest.raises(TypeError):
38+
mq135.measure_concentration()
39+
40+
41+
def test_measure_r0(mq135):
42+
assert mq135.measure_r0(CALIBRATION_CONCENTRATION) == pytest.approx(R0)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy