|
| 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 | + ) |
0 commit comments