Skip to content

Commit fed82d9

Browse files
authored
feat: Implement MQ-135 Gas Sensor (#3244)
1 parent 003cd09 commit fed82d9

9 files changed

Lines changed: 780 additions & 48 deletions

File tree

607 KB
Loading

lib/l10n/app_en.arb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,16 @@
681681
"comingSoon": "Coming Soon",
682682
"startRecording": "Start Recording",
683683
"stopRecording": "Stop Recording",
684-
"tsl2561": "TSL2561",
684+
"gasSensorGuideIntro": "To use this instrument, you need an MQ-135 semiconductor gas sensor.",
685+
"gasSensorGuideDetail": "The MQ-135 is primarily intended for detecting flammable gases and broad air quality, but its readings can be mathematically translated to estimate CO2 Parts Per Million (PPM).",
686+
"gasSensorGuideConnectLabel": "How to connect:",
687+
"gasSensorGuideConnectStep1": "1. Connect the PSLab +5V pin to the sensor's VCC pin.",
688+
"gasSensorGuideConnectStep2": "2. Connect the PSLab GND pin to the sensor's GND pin.",
689+
"gasSensorGuideConnectStep3": "3. Connect the PSLab CH1 pin to the sensor's Analog Out (A0) pin.",
690+
"gasSensorGuideWarning": "Note: Readings are estimated based on default load and baseline resistances and assume a fresh air environment during calibration.",
691+
"ppmCO2" : "PPM CO₂",
692+
"noGasSensor" : "Gas Sensor Unavailable Please connect Gas Sensor",
693+
"tsl2561" : "TSL2561",
685694
"full": "Full Spectrum",
686695
"infrared": "Infrared",
687696
"infraredRaw": "Infrared (raw)",

lib/main.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'package:pslab/view/accelerometer_screen.dart';
1717
import 'package:pslab/view/barometer_screen.dart';
1818
import 'package:pslab/view/connect_device_screen.dart';
1919
import 'package:pslab/view/faq_screen.dart';
20+
import 'package:pslab/view/gas_sensor_screen.dart';
2021
import 'package:pslab/view/gyroscope_screen.dart';
2122
import 'package:pslab/view/instruments_screen.dart';
2223
import 'package:pslab/view/logged_data_screen.dart';
@@ -142,6 +143,8 @@ class MyApp extends StatelessWidget {
142143
const _LocaleAware(child: SoundMeterScreen()),
143144
'/thermometer': (context) =>
144145
const _LocaleAware(child: ThermometerScreen()),
146+
'/gassensor': (context) =>
147+
const _LocaleAware(child: GasSensorScreen()),
145148
'/sensors': (context) =>
146149
const _LocaleAware(child: SensorsScreen()),
147150
'/experiments': (context) =>
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import 'dart:async';
2+
import 'dart:math';
3+
import 'package:fl_chart/fl_chart.dart';
4+
import 'package:flutter/material.dart';
5+
import 'package:pslab/others/logger_service.dart';
6+
import 'package:pslab/communication/science_lab.dart';
7+
8+
import '../l10n/app_localizations.dart';
9+
import 'locator.dart';
10+
11+
class GasSensorStateProvider extends ChangeNotifier {
12+
AppLocalizations appLocalizations = getIt.get<AppLocalizations>();
13+
ScienceLab? _scienceLab;
14+
15+
double _currentPpm = 0.0;
16+
Timer? _readTimer;
17+
18+
final List<double> _ppmData = [];
19+
final List<double> _timeData = [];
20+
final List<FlSpot> _gasChartData = [];
21+
22+
double _startTime = 0;
23+
double _currentTime = 0;
24+
final int _maxLength = 80;
25+
26+
double _ppmMin = double.infinity;
27+
double _ppmMax = 0;
28+
double _ppmSum = 0;
29+
int _dataCount = 0;
30+
31+
bool _isSensorAvailable = false;
32+
bool _isInitialized = false;
33+
bool _isFetching = false;
34+
bool _isCalibrating = false;
35+
36+
final double _rLoad = 10.0;
37+
double _r0 = 41.7;
38+
final double _vcc = 5.0;
39+
40+
final double _paramA = 109.0;
41+
final double _paramB = -2.88;
42+
43+
double get _correction {
44+
double t = 20.0;
45+
double h = 0.65;
46+
double a = 3.28e-4;
47+
double b = -2.55e-2;
48+
double c = 1.38;
49+
double d = -2.24e-1;
50+
return (a * pow(t, 2)) + (b * t) + c + (d * (h - 0.65));
51+
}
52+
53+
double _calculateRs(double voltage) {
54+
if (voltage <= 0.01) voltage = 0.01;
55+
return ((_vcc / voltage) - 1) * _rLoad / _correction;
56+
}
57+
58+
Future<void> initializeSensors() async {
59+
if (_isInitialized) return;
60+
61+
try {
62+
_scienceLab = getIt.get<ScienceLab>();
63+
64+
if (_scienceLab != null && _scienceLab!.isConnected()) {
65+
_isSensorAvailable = true;
66+
_startTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
67+
68+
_isCalibrating = true;
69+
notifyListeners();
70+
71+
await calibrateInFreshAir();
72+
73+
_isCalibrating = false;
74+
75+
_startReadingGasData();
76+
logger.d('Gas sensor initialized successfully');
77+
} else {
78+
_isSensorAvailable = false;
79+
logger.w("ScienceLab not connected");
80+
}
81+
82+
_isInitialized = true;
83+
notifyListeners();
84+
} catch (e) {
85+
logger.e("Error initializing gas sensor: $e");
86+
_isSensorAvailable = false;
87+
_isInitialized = true;
88+
notifyListeners();
89+
}
90+
}
91+
92+
Future<void> calibrateInFreshAir() async {
93+
if (_scienceLab == null || !_scienceLab!.isConnected()) return;
94+
List<double> rsReadings = [];
95+
96+
for (int i = 0; i < 10; i++) {
97+
double volt = await _scienceLab!.getVoltage(appLocalizations.ch1, 1);
98+
if (volt > 4.99) volt = 4.99;
99+
100+
double rs = _calculateRs(volt);
101+
rsReadings.add(rs);
102+
103+
await Future.delayed(const Duration(milliseconds: 100));
104+
}
105+
106+
double avgRs = rsReadings.reduce((a, b) => a + b) / rsReadings.length;
107+
108+
double ratioFactor = pow((400.0 / _paramA), (1.0 / -_paramB)).toDouble();
109+
_r0 = avgRs * ratioFactor;
110+
111+
notifyListeners();
112+
}
113+
114+
void _startReadingGasData() {
115+
_readTimer?.cancel();
116+
117+
_readTimer =
118+
Timer.periodic(const Duration(milliseconds: 2000), (timer) async {
119+
if (!_isSensorAvailable ||
120+
_scienceLab == null ||
121+
_isFetching ||
122+
_isCalibrating) {
123+
return;
124+
}
125+
126+
_isFetching = true;
127+
128+
try {
129+
double volt = await _scienceLab!.getVoltage(appLocalizations.ch1, 1);
130+
if (volt > 4.99) volt = 4.99;
131+
132+
double rs = _calculateRs(volt);
133+
double ratio = rs / _r0;
134+
double ppm = _paramA * pow(ratio, _paramB);
135+
136+
if (ppm < 0 || ppm.isNaN) ppm = 0;
137+
if (ppm > 10000) ppm = 10000;
138+
139+
logger.d(
140+
"READING: Volt: ${volt.toStringAsFixed(3)}V | Rs: ${rs.toStringAsFixed(2)} | R0: ${_r0.toStringAsFixed(2)} | Ratio: ${ratio.toStringAsFixed(3)} | Est PPM: ${ppm.toStringAsFixed(1)}");
141+
142+
_currentPpm = ppm;
143+
_currentTime =
144+
(DateTime.now().millisecondsSinceEpoch / 1000.0) - _startTime;
145+
146+
_updateData();
147+
notifyListeners();
148+
} catch (e) {
149+
logger.e("Gas sensor read error: $e");
150+
} finally {
151+
_isFetching = false;
152+
}
153+
});
154+
}
155+
156+
void disposeSensors() {
157+
_readTimer?.cancel();
158+
_isInitialized = false;
159+
}
160+
161+
@override
162+
void dispose() {
163+
disposeSensors();
164+
super.dispose();
165+
}
166+
167+
void _updateData() {
168+
_ppmData.add(_currentPpm);
169+
_timeData.add(_currentTime);
170+
_ppmSum += _currentPpm;
171+
_dataCount++;
172+
173+
if (_ppmData.length > _maxLength) {
174+
final removedValue = _ppmData.removeAt(0);
175+
_timeData.removeAt(0);
176+
_ppmSum -= removedValue;
177+
_dataCount--;
178+
}
179+
180+
if (_ppmData.isNotEmpty) {
181+
_ppmMin = _ppmData.reduce(min);
182+
_ppmMax = _ppmData.reduce(max);
183+
}
184+
185+
_gasChartData.clear();
186+
for (int i = 0; i < _ppmData.length; i++) {
187+
_gasChartData.add(FlSpot(_timeData[i], _ppmData[i]));
188+
}
189+
}
190+
191+
double getCurrentPpm() => _currentPpm;
192+
double getMinPpm() => _ppmMin == double.infinity ? 0 : _ppmMin;
193+
double getMaxPpm() => _ppmMax;
194+
double getAveragePpm() => _dataCount > 0 ? _ppmSum / _dataCount : 0.0;
195+
List<FlSpot> getGasChartData() =>
196+
_gasChartData.isEmpty ? [const FlSpot(0, 0)] : _gasChartData;
197+
int getDataLength() => _gasChartData.length;
198+
double getCurrentTime() => _currentTime;
199+
double getMaxTime() => _timeData.isNotEmpty ? _timeData.last : 0;
200+
double getMinTime() => _timeData.isNotEmpty ? _timeData.first : 0;
201+
bool isSensorAvailable() => _isSensorAvailable;
202+
bool isInitialized() => _isInitialized;
203+
204+
bool isCalibrating() => _isCalibrating;
205+
206+
double getTimeInterval() {
207+
if (_currentTime <= 10) return 2;
208+
if (_currentTime <= 30) return 5;
209+
if (_currentTime <= 80) return 10;
210+
return 20;
211+
}
212+
}

lib/theme/colors.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,6 @@ Color buttonDisabledColor = Color.fromARGB(255, 240, 162, 162);
9191
Color optionDisabledColor = Colors.black38;
9292
Color waveGeneratorPropTextColor = Colors.deepOrange;
9393
List<Color> stepCompletedColor = [Colors.green.shade400, Colors.green.shade600];
94+
const Color gaugeGradientStart = Color(0xFF40C4FF);
95+
const Color gaugeGradientCenter = Color(0xFF00B0FF);
96+
const Color gaugeGradientEnd = Color(0xFF0091EA);

0 commit comments

Comments
 (0)