Skip to content

Conversation

@manishraj-003
Copy link
Contributor

@manishraj-003 manishraj-003 commented Dec 26, 2025

Fixes #2987

Changes

New Files

  • lib/communication/sensors/mq135.dart - MQ-135 sensor interface

    • Reads analog voltage from PSLab CH1 pin
    • Converts voltage to PPM using characteristic curve formula
    • Includes simulated data collection for testing
  • lib/providers/gas_sensor_provider.dart - State management provider

    • Manages sensor initialization and data collection
    • Periodic timer-based data reading
    • Support for looping and single-run modes
    • Real-time PPM calculation
  • lib/view/gas_sensor_screen.dart - UI implementation

    • Displays current gas concentration (PPM)
    • Real-time chart visualization
    • Sensor controls (play/pause, loop, timegap, clear)

Modified Files

  • lib/main.dart

    • Added import: import 'package:pslab/view/gas_sensor_screen.dart';
    • Added route: '/gassensor': (context) => const GasSensorScreen(),
  • lib/view/instruments_screen.dart

    • Uncommented gas sensor in instrument list
    • Gas sensor now visible and accessible in instruments screen

Hardware Connection

For real hardware use with MQ-135 sensor:

  • VCC → PSLab VDD (3.3V)
  • GND → PSLab GND
  • AO (Analog Out) → PSLab CH1

Data Collection

  • Timegap: 200-1000ms (configurable)
  • Data Points: Adjustable sample count
  • Looping: Continuous or single-run mode
  • Display: Real-time PPM visualization

Testing

  • Compiles without errors
  • Screen opens and displays correctly
  • Sensor controls functional
  • Follows existing code patterns
  • Simulated data collection working

Screenshots / Recordings

Screen.Recording.2025-12-27.021246.mp4

Checklist:

  • No hard coding: I have used values from constants.dart or localization files instead of hard-coded values.
  • No end of file edits: No modifications done at end of resource files.
  • Code reformatting: I have formatted the code using dart format or the IDE formatter.
  • Code analysis: My code passes checks run in flutter analyze and tests run in flutter 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:

  • Introduce an MQ-135 gas sensor interface that reads analog data from PSLab and converts it to gas concentration in PPM.
  • Add a gas sensor provider to manage MQ-135 initialization, periodic sampling, looping/single-run modes, and data buffering for charts.
  • Create a gas sensor instrument screen showing current gas concentration, a real-time chart, and controls for playback, loop, timing, and clearing data.
  • Expose the gas sensor instrument in the instruments list and register a dedicated navigation route for it.

Enhancements:

  • Register geolocator plugins for macOS and Windows to support platform location services.

Build:

  • Include geolocator plugins in the Windows CMake plugin list and generated registrant files for desktop builds.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Dec 26, 2025

Reviewer's Guide

Implements 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 flow

sequenceDiagram
  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
Loading

Updated class diagram for GasSensorScreen, GasSensorProvider, and MQ135

classDiagram
  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
Loading

File-Level Changes

Change Details Files
Add MQ-135 gas sensor communication layer for reading CH1 analog values and converting them to PPM.
  • Introduce MQ135 class that validates ScienceLab connectivity and exposes a factory constructor for initialization.
  • Simulate ADC readings when PSLab is not connected, generating realistic 12-bit values for development/testing.
  • Implement conversion pipeline from raw ADC value to voltage, sensor resistance, and final PPM using MQ-135 characteristic curve.
  • Add logging around initialization, raw reads, and computed PPM/voltage/resistance values.
lib/communication/sensors/mq135.dart
Introduce GasSensorProvider for timer-driven sampling, chart data management, and UI-facing state.
  • Maintain current PPM value, running/looping flags, configuration (time gap, reading count), and availability state.
  • Initialize MQ135 using an injected ScienceLab, surfacing connection errors via a callback.
  • Run a periodic Timer that fetches sensor data, updates the current time and chart points, and enforces reading-count and max-data limits.
  • Provide setters for time gap and number of readings that restart the timer when needed and expose clear/reset and completion-status helpers.
  • Ensure timers and sensor references are cleaned up in dispose.
lib/providers/gas_sensor_provider.dart
Add Gas Sensor instrument screen with live PPM display, chart, and shared sensor controls wired to the provider.
  • Wrap the screen in a ChangeNotifierProvider and initialize GasSensorProvider with the current ScienceLab instance and error snackbar callback.
  • Display a raw data card with current gas concentration in PPM and a sensor image (with icon fallback).
  • Render a SensorChartWidget configured for gas PPM vs time, using provider.gasPPMData and respecting the configured reading count.
  • Wire SensorControlsWidget callbacks to provider methods for play/pause, looping, timegap, reading count, and clear-data (with success snackbar).
  • Handle localizations and theme colors consistently with existing instruments and dispose the provider on screen teardown.
lib/view/gas_sensor_screen.dart
Wire the new gas sensor screen into navigation and the instruments list.
  • Import GasSensorScreen in the app root and register the '/gassensor' route in MaterialApp routes.
  • Add the gas sensor entry back into the instruments list so it appears alongside other instruments and navigates to the new route.
lib/main.dart
lib/view/instruments_screen.dart
Update desktop plugin registrants to include geolocator plugins on macOS and Windows.
  • Register geolocator_windows in the Windows plugin registrant source and corresponding CMake plugin list.
  • Register geolocator_apple in the macOS GeneratedPluginRegistrant.swift.
windows/flutter/generated_plugin_registrant.cc
windows/flutter/generated_plugins.cmake
macos/Flutter/GeneratedPluginRegistrant.swift

Assessment against linked issues

Issue Objective Addressed Explanation
#2987 Implement a Gas Sensor instrument screen in the Flutter app that provides a dedicated UI for viewing gas sensor data.
#2987 Expose the Gas Sensor instrument in the instruments list and navigation so that it is accessible from the Flutter app, analogous to the old Java version.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a 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 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 119 to 120
onPlayPause: () {
provider.toggleDataCollection();
Copy link
Contributor

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.

Copy link

Copilot AI left a 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 /gassensor route.
  • 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';
Copy link

Copilot AI Feb 8, 2026

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.

Suggested change
final String sensorImage = 'assets/images/mq135.jpg';
final String sensorImage = 'assets/images/mq135.png';

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +66
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);

Copy link

Copilot AI Feb 8, 2026

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).

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +135
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Ω");
Copy link

Copilot AI Feb 8, 2026

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.

Copilot uses AI. Check for mistakes.
/// Sets up the MQ135 sensor interface and validates connection
Future<void> initializeSensors({
required ScienceLab? scienceLab,
required Function(String)? onError,
Copy link

Copilot AI Feb 8, 2026

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).

Suggested change
required Function(String)? onError,
void Function(String)? onError,

Copilot uses AI. Check for mistakes.
Comment on lines +157 to +158
logger.d(
'Gas Sensor: ${_gasPPM.toStringAsFixed(2)} PPM at ${_currentTime.toStringAsFixed(1)}s');
Copy link

Copilot AI Feb 8, 2026

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.

Suggested change
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');
}

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +107
SensorChartWidget(
title:
'${appLocalizations.plot} - ${appLocalizations.gasSensor}',
yAxisLabel: 'Gas Concentration (PPM)',
data: provider.gasPPMData,
lineColor: Colors.orange,
unit: ' PPM',
maxDataPoints: provider.numberOfReadings,
Copy link

Copilot AI Feb 8, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +184 to +188
flex: 2,
child: _buildDataCard(
'Gas Concentration',
'${provider.gasPPM.toStringAsFixed(2)} PPM',
),
Copy link

Copilot AI Feb 8, 2026

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add GAS SENSOR instrument

1 participant