-
Notifications
You must be signed in to change notification settings - Fork 831
feat: Add Gas Sensor (MQ-135) instrument screen #3010
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: flutter
Are you sure you want to change the base?
Conversation
Reviewer's GuideImplements a new MQ-135 gas sensor instrument, including sensor communication, provider-based state management, a UI screen with charting and controls, and wiring it into navigation and the instruments list; also updates desktop plugin registrants for geolocator dependencies. Sequence diagram for MQ135 gas sensor data collection flowsequenceDiagram
actor User
participant InstrumentsScreen
participant GasSensorScreen
participant GasSensorProvider
participant MQ135
participant ScienceLab
User->>InstrumentsScreen: tap_gas_sensor_instrument
InstrumentsScreen->>GasSensorScreen: Navigator.push(/gassensor)
activate GasSensorScreen
GasSensorScreen->>GasSensorProvider: create_via_ChangeNotifierProvider
activate GasSensorProvider
GasSensorScreen->>GasSensorProvider: initializeSensors(scienceLab,_showSensorErrorSnackbar)
GasSensorProvider->>ScienceLab: isConnected()
alt ScienceLab_connected
GasSensorProvider->>MQ135: MQ135.create(scienceLab)
activate MQ135
MQ135-->>GasSensorProvider: MQ135_instance
GasSensorProvider-->>GasSensorScreen: notifyListeners()
else ScienceLab_not_connected
GasSensorProvider-->>GasSensorScreen: onError(pslabNotConnected)
GasSensorScreen->>GasSensorScreen: _showSensorErrorSnackbar()
end
User->>GasSensorScreen: press_play
GasSensorScreen->>GasSensorProvider: toggleDataCollection()
GasSensorProvider->>GasSensorProvider: _startDataCollection()
GasSensorProvider->>GasSensorProvider: start_Timer.periodic(timegapMs)
loop every_timegapMs_while_running
GasSensorProvider->>MQ135: getRawData()
MQ135->>MQ135: readRawValue()
MQ135->>MQ135: _adcToVoltage(rawValue)
MQ135->>MQ135: _calculateResistance(voltage)
MQ135->>MQ135: _calculatePPM(resistance)
MQ135-->>GasSensorProvider: {ppm,raw,voltage}
GasSensorProvider->>GasSensorProvider: _addDataPoint(gasPPMData,gasPPM)
GasSensorProvider->>GasSensorProvider: update_collectedReadings_and_currentTime
alt not_isLooping_and_collectedReadings>=numberOfReadings
GasSensorProvider->>GasSensorProvider: _stopDataCollection()
GasSensorProvider-->>GasSensorScreen: notifyListeners()
GasSensorProvider->>GasSensorProvider: break_loop
else isLooping_and_gasPPMData_exceeds_max
GasSensorProvider->>GasSensorProvider: _removeOldestDataPoints()
end
GasSensorProvider-->>GasSensorScreen: notifyListeners()
GasSensorScreen->>GasSensorScreen: redraw_UI_and_chart
end
User->>GasSensorScreen: press_clear
GasSensorScreen->>GasSensorProvider: clearData()
GasSensorProvider-->>GasSensorScreen: notifyListeners()
GasSensorScreen->>GasSensorScreen: _showSuccessSnackbar(dataCleared)
User-->>GasSensorScreen: navigate_back
GasSensorScreen->>GasSensorProvider: dispose()
GasSensorProvider->>GasSensorProvider: _stopDataCollection()
deactivate GasSensorProvider
deactivate GasSensorScreen
Updated class diagram for GasSensorScreen, GasSensorProvider, and MQ135classDiagram
class GasSensorScreen {
+GasSensorScreen()
}
class _GasSensorScreenState {
+AppLocalizations appLocalizations
+String sensorImage
+ScienceLab _scienceLab
+GasSensorProvider _provider
+initState()
+build(BuildContext context) Widget
+dispose()
-_initializeScienceLab() void
-_showSensorErrorSnackbar(String message) void
-_showSuccessSnackbar(String message) void
-_buildRawDataSection(GasSensorProvider provider) Widget
-_buildDataCard(String label, String value) Widget
}
class GasSensorProvider {
+AppLocalizations appLocalizations
-MQ135 _mq135
-Timer _dataTimer
-double _gasPPM
-bool _isRunning
-bool _isLooping
-bool _isSensorAvailable
-int _timegapMs
-int _numberOfReadings
-int _collectedReadings
-double _currentTime
-List~ChartDataPoint~ _gasPPMData
+static int maxDataPoints
+double get gasPPM
+bool get isRunning
+bool get isLooping
+bool get isSensorAvailable
+int get timegapMs
+int get numberOfReadings
+int get collectedReadings
+List~ChartDataPoint~ get gasPPMData
+GasSensorProvider()
+Future~void~ initializeSensors(ScienceLab scienceLab, Function onError)
+void toggleDataCollection()
+void toggleLooping()
+void setTimegap(int value)
+void setNumberOfReadings(int value)
+void clearData()
+bool get isCollectionComplete
+void dispose()
-void _startDataCollection()
-void _stopDataCollection()
-Future~void~ _fetchSensorData()
-void _addDataPoint(List~ChartDataPoint~ dataList, double value)
-void _removeOldestDataPoints()
}
class MQ135 {
+AppLocalizations appLocalizations
+static String tag
+static double ro
+static double mvPerDiv
+static double adcVcc
+static double gasConstant
+static double powerValue
+ScienceLab scienceLab
-int _lastRawValue
-double _gasPPM
+MQ135(ScienceLab scienceLab)
+static Future~MQ135~ create(ScienceLab scienceLab)
+Future~int~ readRawValue()
+Future~Map~ getRawData()
+double get gasPPM
+int get lastRawValue
-double _adcToVoltage(int rawValue)
-double _calculateResistance(double voltage)
-double _calculatePPM(double resistance)
}
class ScienceLab {
+bool isConnected()
}
class ChartDataPoint {
+double x
+double y
}
GasSensorScreen ..> _GasSensorScreenState : creates_state
_GasSensorScreenState ..> GasSensorProvider : uses_as_provider
_GasSensorScreenState ..> ScienceLab : uses_for_hardware
GasSensorProvider --> MQ135 : composes
GasSensorProvider ..> ChartDataPoint : uses_for_chart_data
MQ135 --> ScienceLab : composes
GasSensorProvider ..|> ChangeNotifier
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - I've found 3 issues, and left some high level feedback:
- In
GasSensorScreen, you're passing aGasSensorProviderinstance toChangeNotifierProvider'screateand then also calling_provider.dispose()in the widget'sdispose(), which leads to double-dispose risk; you can rely onChangeNotifierProviderto handle disposal and remove the manual_provider.dispose()call (and possibly the_providerfield altogether). - In
GasSensorProvider.initializeSensors, the error callback uses a hard-coded English string ('Error initializing Gas Sensor: $e') instead of localization, which breaks the existing localization pattern; consider moving this message intoAppLocalizationsand composing the error string from localized parts. - In
MQ135.readRawValue, the method always returns simulated values and never reads from the actualScienceLabeven when connected, which can be confusing in production use; consider either wiring this to the real analog read or adding a clear configuration flag / TODO to distinguish simulation mode from real hardware mode.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `GasSensorScreen`, you're passing a `GasSensorProvider` instance to `ChangeNotifierProvider`'s `create` and then also calling `_provider.dispose()` in the widget's `dispose()`, which leads to double-dispose risk; you can rely on `ChangeNotifierProvider` to handle disposal and remove the manual `_provider.dispose()` call (and possibly the `_provider` field altogether).
- In `GasSensorProvider.initializeSensors`, the error callback uses a hard-coded English string (`'Error initializing Gas Sensor: $e'`) instead of localization, which breaks the existing localization pattern; consider moving this message into `AppLocalizations` and composing the error string from localized parts.
- In `MQ135.readRawValue`, the method always returns simulated values and never reads from the actual `ScienceLab` even when connected, which can be confusing in production use; consider either wiring this to the real analog read or adding a clear configuration flag / TODO to distinguish simulation mode from real hardware mode.
## Individual Comments
### Comment 1
<location> `lib/view/gas_sensor_screen.dart:16-25` </location>
<code_context>
+ return !_isLooping && _collectedReadings >= _numberOfReadings;
+ }
+
+ @override
+ void dispose() {
+ _stopDataCollection();
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid manually disposing the provider since ChangeNotifierProvider will dispose it automatically.
Since this is provided via `ChangeNotifierProvider(create: ...)`, Flutter will automatically call `dispose()` on `GasSensorProvider` when the widget is removed. Manually calling `_provider.dispose()` in `State.dispose()` risks a double-dispose and runtime errors, so it should be removed.
</issue_to_address>
### Comment 2
<location> `lib/view/gas_sensor_screen.dart:119-120` </location>
<code_context>
+ isLooping: provider.isLooping,
+ timegapMs: provider.timegapMs,
+ numberOfReadings: provider.numberOfReadings,
+ onPlayPause: () {
+ provider.toggleDataCollection();
+ },
+ onLoop: provider.toggleLooping,
</code_context>
<issue_to_address>
**suggestion:** Consider handling the "sensor not available" case in the UI when play is pressed.
Right now, when the sensor is unavailable, `toggleDataCollection` only logs a warning and `_startDataCollection` returns, so the UI appears unresponsive. Since `GasSensorProvider` exposes `isSensorAvailable`, consider using it here to disable the play button or show an error/snackbar before calling `toggleDataCollection()`, so users know why acquisition doesn’t start.
</issue_to_address>
### Comment 3
<location> `lib/providers/gas_sensor_provider.dart:59-68` </location>
<code_context>
+ }) async {
+ try {
+ if (scienceLab == null) {
+ _isSensorAvailable = false;
+ onError?.call(appLocalizations.pslabNotConnected);
+ logger.w('ScienceLab not available');
+ return;
+ }
+
+ if (!scienceLab.isConnected()) {
+ _isSensorAvailable = false;
+ onError?.call(appLocalizations.pslabNotConnected);
+ logger.w('ScienceLab not connected');
+ return;
+ }
+
+ // Create MQ135 sensor instance
+ _mq135 = await MQ135.create(scienceLab);
+ _isSensorAvailable = true;
+
+ logger.d('Gas Sensor Provider initialized successfully');
+ notifyListeners();
+ } catch (e) {
+ _isSensorAvailable = false;
+ logger.e('Error initializing Gas Sensor: $e');
+ onError?.call('Error initializing Gas Sensor: $e');
+ notifyListeners();
+ }
</code_context>
<issue_to_address>
**suggestion:** Initialization error message is hardcoded instead of using localization.
In the catch block, the error passed to `onError` is a hardcoded English string instead of a localized value. Please use `appLocalizations` (or build the string from localized parts) here as well for consistency and i18n support.
Suggested implementation:
```
} catch (e) {
_isSensorAvailable = false;
logger.e('Error initializing Gas Sensor: $e');
// Use localized error message, optionally including the error detail
final localizedMessage =
'${appLocalizations.gasSensorInitializationError}: $e';
onError?.call(localizedMessage);
notifyListeners();
}
```
To fully support this change you will also need to:
1. Add a `gasSensorInitializationError` string to your localization ARB files (e.g. `app_en.arb`, `app_xx.arb`) with an appropriate translated value (for example: `"gasSensorInitializationError": "Error initializing gas sensor"`).
2. Regenerate the localization classes (e.g. by running `flutter gen-l10n` or your project’s standard i18n generation command) so that `appLocalizations.gasSensorInitializationError` is available.
3. Optionally, if you prefer structured error messages, you could instead define a message that takes the error detail as an argument (e.g. `gasSensorInitializationError(String details)`) and adjust the code to call that generated method accordingly.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
lib/view/gas_sensor_screen.dart
Outdated
| onPlayPause: () { | ||
| provider.toggleDataCollection(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Consider handling the "sensor not available" case in the UI when play is pressed.
Right now, when the sensor is unavailable, toggleDataCollection only logs a warning and _startDataCollection returns, so the UI appears unresponsive. Since GasSensorProvider exposes isSensorAvailable, consider using it here to disable the play button or show an error/snackbar before calling toggleDataCollection(), so users know why acquisition doesn’t start.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds an MQ-135 “Gas Sensor” instrument to the PSLab Flutter app, wiring it into navigation and the instruments list, with a provider-driven live chart UI and supporting localization/plugin registration updates.
Changes:
- Introduces MQ-135 sensor interface, gas sensor state provider, and a new gas sensor instrument screen with chart + controls.
- Exposes the gas sensor in the instruments list and registers a
/gassensorroute. - Updates localization outputs and desktop plugin registrants (geolocator), plus lockfile updates.
Reviewed changes
Copilot reviewed 24 out of 25 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| windows/flutter/generated_plugins.cmake | Registers geolocator_windows in Windows plugin list. |
| windows/flutter/generated_plugin_registrant.cc | Registers geolocator plugin for Windows builds. |
| macos/Flutter/GeneratedPluginRegistrant.swift | Registers geolocator plugin for macOS builds. |
| pubspec.lock | Updates locked dependency versions and recorded SDK constraints. |
| lib/main.dart | Adds import and route for the gas sensor screen. |
| lib/view/instruments_screen.dart | Makes gas sensor visible/launchable from Instruments list. |
| lib/view/gas_sensor_screen.dart | New UI screen for displaying PPM + chart and controls. |
| lib/providers/gas_sensor_provider.dart | New provider managing MQ-135 lifecycle and periodic sampling. |
| lib/communication/sensors/mq135.dart | New MQ-135 sensor abstraction + PPM conversion logic. |
| lib/l10n/app_en.arb | Adds new localization keys (e.g., gasSensorInitError). |
| lib/l10n/app_localizations.dart | Regenerated localization interface with new getters. |
| lib/l10n/app_localizations_en.dart | Regenerated English localization implementation. |
| lib/l10n/app_localizations_de.dart | Regenerated German localization implementation. |
| lib/l10n/app_localizations_es.dart | Regenerated Spanish localization implementation. |
| lib/l10n/app_localizations_fr.dart | Regenerated French localization implementation. |
| lib/l10n/app_localizations_he.dart | Regenerated Hebrew localization implementation. |
| lib/l10n/app_localizations_hi.dart | Regenerated Hindi localization implementation. |
| lib/l10n/app_localizations_id.dart | Regenerated Indonesian localization implementation. |
| lib/l10n/app_localizations_ja.dart | Regenerated Japanese localization implementation. |
| lib/l10n/app_localizations_nb.dart | Regenerated Norwegian Bokmål localization implementation. |
| lib/l10n/app_localizations_pt.dart | Regenerated Portuguese localization implementation. |
| lib/l10n/app_localizations_ru.dart | Regenerated Russian localization implementation. |
| lib/l10n/app_localizations_uk.dart | Regenerated Ukrainian localization implementation. |
| lib/l10n/app_localizations_vi.dart | Regenerated Vietnamese localization implementation. |
| lib/l10n/app_localizations_zh.dart | Regenerated Chinese localization implementation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| class _GasSensorScreenState extends State<GasSensorScreen> { | ||
| final AppLocalizations appLocalizations = getIt.get<AppLocalizations>(); | ||
| final String sensorImage = 'assets/images/mq135.jpg'; |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The screen references assets/images/mq135.jpg, but that file is not present under assets/images/ in the repo. As a result the UI will always fall back to the errorBuilder icon. Add the missing asset (and ensure it’s packaged), or point to an existing image.
| final String sensorImage = 'assets/images/mq135.jpg'; | |
| final String sensorImage = 'assets/images/mq135.png'; |
| Future<int> readRawValue() async { | ||
| try { | ||
| // In a real implementation, this would read from the PSLab hardware | ||
| // For now, we'll simulate sensor readings for demonstration | ||
| if (!scienceLab.isConnected()) { | ||
| logger.w("PSLab not connected, returning simulated value"); | ||
| } | ||
|
|
||
| // Simulate realistic sensor data with some variation | ||
| // Typical values: 0-4095 for 12-bit ADC | ||
| // We'll simulate values in the range 800-2500 for normal air quality | ||
| final now = DateTime.now().millisecondsSinceEpoch; | ||
| final baseValue = 1500 + ((now ~/ 100) % 1000) - 500; | ||
| _lastRawValue = baseValue.clamp(0, 4095); | ||
|
|
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
readRawValue() currently generates simulated ADC values regardless of whether PSLab is connected. This contradicts the stated behavior (reading CH1 analog voltage) and means real hardware measurements will never be used. Consider reading CH1 via ScienceLab (e.g., voltage/ADC APIs) when connected, and only falling back to simulation behind an explicit debug/emulator flag (or return an error when disconnected).
| final now = DateTime.now().millisecondsSinceEpoch; | ||
| final baseValue = 1500 + ((now ~/ 100) % 1000) - 500; | ||
| _lastRawValue = baseValue.clamp(0, 4095); | ||
|
|
||
| logger.d("$tag: Raw ADC value = $_lastRawValue"); | ||
| return _lastRawValue; | ||
| } catch (e) { | ||
| logger.e("$tag: Error reading raw value: $e"); | ||
| rethrow; | ||
| } | ||
| } | ||
|
|
||
| /// Convert raw ADC value to voltage in mV | ||
| double _adcToVoltage(int rawValue) { | ||
| return (rawValue / 4095.0) * adcVcc * 1000.0; | ||
| } | ||
|
|
||
| /// Calculate resistance of the sensor | ||
| /// | ||
| /// R_sensor = (V_cc - V_out) * R_load / V_out | ||
| /// For simplicity, we assume fixed load resistance | ||
| double _calculateResistance(double voltage) { | ||
| final vOut = voltage / 1000.0; // Convert to volts | ||
|
|
||
| if (vOut >= adcVcc || vOut <= 0) { | ||
| logger.w("$tag: Voltage out of expected range: $vOut V"); | ||
| return ro; // Return base resistance if out of range | ||
| } | ||
|
|
||
| const rLoad = 10.0; // Load resistor in k-ohms | ||
| final resistance = (adcVcc - vOut) * rLoad / vOut; | ||
|
|
||
| return resistance.clamp(0.0, 1000.0); // Clamp to reasonable values | ||
| } | ||
|
|
||
| /// Calculate PPM from sensor resistance | ||
| /// | ||
| /// Uses the MQ-135 characteristic curve: | ||
| /// ppm = a * (Rs/Ro)^b | ||
| /// | ||
| /// For CO2 detection (common use case): | ||
| /// a = 116.6020682, b = -2.769034857 | ||
| double _calculatePPM(double resistance) { | ||
| if (resistance <= 0 || ro <= 0) { | ||
| logger.w("$tag: Invalid resistance values for PPM calculation"); | ||
| return 0.0; | ||
| } | ||
|
|
||
| final ratio = resistance / ro; | ||
| final ppm = gasConstant * math.pow(ratio, powerValue); | ||
|
|
||
| // Clamp PPM to realistic values (0-5000 ppm) | ||
| return ppm.clamp(0.0, 5000.0); | ||
| } | ||
|
|
||
| /// Read and calculate gas concentration in PPM | ||
| /// | ||
| /// Returns a map with: | ||
| /// - 'ppm': Gas concentration in parts per million | ||
| /// - 'raw': Raw ADC value | ||
| /// - 'voltage': Sensor voltage in mV | ||
| Future<Map<String, double>> getRawData() async { | ||
| try { | ||
| final rawValue = await readRawValue(); | ||
| final voltage = _adcToVoltage(rawValue); | ||
| final resistance = _calculateResistance(voltage); | ||
| final ppm = _calculatePPM(resistance); | ||
|
|
||
| _gasPPM = ppm; | ||
|
|
||
| logger.d("$tag: PPM = ${ppm.toStringAsFixed(2)}, " | ||
| "Voltage = ${voltage.toStringAsFixed(2)} mV, " | ||
| "Resistance = ${resistance.toStringAsFixed(2)} kΩ"); |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code logs debug output on every sample (raw value + computed PPM/voltage/resistance). At 200–1000ms intervals this can flood logs and impact performance on slower devices. Consider removing per-sample logger.d calls or gating them behind a debug flag / lower log level, keeping only error logs and occasional summary logs.
| /// Sets up the MQ135 sensor interface and validates connection | ||
| Future<void> initializeSensors({ | ||
| required ScienceLab? scienceLab, | ||
| required Function(String)? onError, |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
initializeSensors declares required Function(String)? onError (required but nullable), which is an awkward API contract. Align with the other sensor providers by using either required void Function(String) onError (non-nullable) or make it optional without required (e.g., void Function(String)? onError).
| required Function(String)? onError, | |
| void Function(String)? onError, |
| logger.d( | ||
| 'Gas Sensor: ${_gasPPM.toStringAsFixed(2)} PPM at ${_currentTime.toStringAsFixed(1)}s'); |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per-sample debug logging inside _fetchSensorData() will run at the timer interval and can significantly spam logs. Other providers typically avoid logging on every tick. Consider removing this or sampling it (e.g., every N points) / gating behind a debug flag.
| logger.d( | |
| 'Gas Sensor: ${_gasPPM.toStringAsFixed(2)} PPM at ${_currentTime.toStringAsFixed(1)}s'); | |
| if (kDebugMode) { | |
| logger.d( | |
| 'Gas Sensor: ${_gasPPM.toStringAsFixed(2)} PPM at ${_currentTime.toStringAsFixed(1)}s'); | |
| } |
| SensorChartWidget( | ||
| title: | ||
| '${appLocalizations.plot} - ${appLocalizations.gasSensor}', | ||
| yAxisLabel: 'Gas Concentration (PPM)', | ||
| data: provider.gasPPMData, | ||
| lineColor: Colors.orange, | ||
| unit: ' PPM', | ||
| maxDataPoints: provider.numberOfReadings, |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Several user-visible strings/units are hard-coded (e.g., y-axis label and unit). Other instrument screens build these labels from appLocalizations (see ADS1115Screen / BMP180Screen). Consider adding appropriate localization keys (e.g., gas concentration + unit) and using them here to keep UI consistent across locales.
| flex: 2, | ||
| child: _buildDataCard( | ||
| 'Gas Concentration', | ||
| '${provider.gasPPM.toStringAsFixed(2)} PPM', | ||
| ), |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The raw-data label/value formatting uses hard-coded text ('Gas Concentration' and 'PPM'). This should be localized similarly to other sensor screens (and ideally reuse a localized unit label) so the UI is consistent for non-English locales.
Fixes #2987
Changes
New Files
lib/communication/sensors/mq135.dart - MQ-135 sensor interface
lib/providers/gas_sensor_provider.dart - State management provider
lib/view/gas_sensor_screen.dart - UI implementation
Modified Files
lib/main.dart
import 'package:pslab/view/gas_sensor_screen.dart';'/gassensor': (context) => const GasSensorScreen(),lib/view/instruments_screen.dart
Hardware Connection
For real hardware use with MQ-135 sensor:
Data Collection
Testing
Screenshots / Recordings
Screen.Recording.2025-12-27.021246.mp4
Checklist:
constants.dartor localization files instead of hard-coded values.dart formator the IDE formatter.flutter analyzeand tests run influtter test.Summary by Sourcery
Add a new MQ-135 gas sensor instrument with data visualization and wire it into the app navigation and platform plugins.
New Features:
Enhancements:
Build: