Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/images/ccs811.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
129 changes: 129 additions & 0 deletions lib/communication/sensors/ccs811.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import 'dart:async';
import 'package:pslab/communication/peripherals/i2c.dart';
import 'package:pslab/communication/science_lab.dart';
import 'package:pslab/others/logger_service.dart';

class CCS811 {
static const String tag = "CCS811";
static const int address = 0x5A;

static const int algResultData = 0x02;
static const int hwId = 0x20;
static const int fwBootVersion = 0x23;
static const int fwAppVersion = 0x24;
static const int measMode = 0x01;
static const int hwVersion = 0x21;
static const int appStart = 0xF4;

static const int driveMode1Sec = 0x01;

final I2C i2c;

CCS811._(this.i2c);

static Future<CCS811> create(I2C i2c, ScienceLab scienceLab) async {
final ccs811 = CCS811._(i2c);
if (scienceLab.isConnected()) {
await ccs811._initialize();
}
return ccs811;
}

Future<void> _initialize() async {
try {
await _fetchID();
await _appStart();
await Future.delayed(const Duration(milliseconds: 100));
await _disableInterrupt();
await _setMeasMode();
} catch (e) {
logger.e("$tag Error initializing: $e");
rethrow;
}
}

Future<void> _setMeasMode() async {
int config = (1 << 2) | (driveMode1Sec << 4);
await i2c.write(address, [config], measMode);
}

Future<void> _disableInterrupt() async {
int config = (1 << 2) | (3 << 4);
await i2c.write(address, [config], measMode);
}

Future<void> _fetchID() async {
try {
List<int> hwIdData = await i2c.readBulk(address, hwId, 1);
await Future.delayed(const Duration(milliseconds: 20));

List<int> hwVerData = await i2c.readBulk(address, hwVersion, 1);
await Future.delayed(const Duration(milliseconds: 20));

List<int> bootVerData = await i2c.readBulk(address, fwBootVersion, 2);
await Future.delayed(const Duration(milliseconds: 20));

List<int> appVerData = await i2c.readBulk(address, fwAppVersion, 2);
await Future.delayed(const Duration(milliseconds: 20));

if (hwIdData.isNotEmpty) {
logger.d("$tag Hardware ID: ${hwIdData[0] & 0xFF}");
}
if (hwVerData.isNotEmpty) {
logger.d("$tag Hardware Version: ${hwVerData[0] & 0xFF}");
}
if (bootVerData.length >= 2) {
logger
.d("$tag Boot Version: ${(bootVerData[0] << 8) | bootVerData[1]}");
}
if (appVerData.length >= 2) {
logger.d("$tag App Version: ${(appVerData[0] << 8) | appVerData[1]}");
}
} catch (e) {
logger.e("$tag Error fetching IDs: $e");
}
}

Future<void> _appStart() async {
await i2c.write(address, [], appStart);
}

String _decodeError(int error) {
String e = "";
if ((error & 1) > 0) e += ", Invalid register address ID";
if ((error & (1 << 1)) > 0) e += ", Invalid mailbox ID";
if ((error & (1 << 2)) > 0) e += ", Unsupported mode to MEAS_MODE";
if ((error & (1 << 3)) > 0) {
e += ", Resistance measurement max range reached";
}
if ((error & (1 << 4)) > 0) e += ", Heater current out of range";
if ((error & (1 << 5)) > 0) e += ", Heater voltage applied incorrectly";

return e.isNotEmpty ? "Error: ${e.substring(2)}" : "Unknown error";
}

Future<Map<String, int>> getRawData() async {
try {
List<int> data = await i2c.readBulk(address, algResultData, 8);
if (data.length < 8) {
throw Exception("Expected 8 bytes but got ${data.length}");
}

int eCO2 = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF);
int tvoc = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF);
int errorId = data[5] & 0xFF;

if (errorId > 0) {
logger.e("$tag ${_decodeError(errorId)}");
}

return {
'eCO2': eCO2,
'TVOC': tvoc,
};
} catch (e) {
logger.e("$tag Error getting raw data: $e");
rethrow;
}
}
}
11 changes: 10 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -763,5 +763,14 @@
"sensorDescVL53L0X": "Time-of-Flight (ToF) Laser Distance",
"mightNotConnected" : "might not be connected.",
"hmc5883l" : "HMC5883L",
"gauss" : "Gauss"
"gauss" : "Gauss",
"i2cError" : "Check I2C wiring.",
"ccs811" : "CCS811",
"eco2" : "eCO2",
"tvoc" : "TVOC",
"ppb" : "ppb",
"concentrationPpm" : "Concentration (PPM)",
"gasSensorConfig" : "Gas Sensor Configurations",
"gasSensorPeriodHint" : "Enter update period (100 - 5000 ms)",
"mq135" : "MQ-135"
}
39 changes: 39 additions & 0 deletions lib/models/gas_sensor_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class GasSensorConfig {
final int updatePeriod;
final String activeSensor;
final bool includeLocationData;

const GasSensorConfig({
this.updatePeriod = 2000,
this.activeSensor = 'MQ-135',
this.includeLocationData = true,
});

GasSensorConfig copyWith({
int? updatePeriod,
String? activeSensor,
bool? includeLocationData,
}) {
return GasSensorConfig(
updatePeriod: updatePeriod ?? this.updatePeriod,
activeSensor: activeSensor ?? this.activeSensor,
includeLocationData: includeLocationData ?? this.includeLocationData,
);
}

Map<String, dynamic> toJson() {
return {
'updatePeriod': updatePeriod,
'activeSensor': activeSensor,
'includeLocationData': includeLocationData,
};
}

factory GasSensorConfig.fromJson(Map<String, dynamic> json) {
return GasSensorConfig(
updatePeriod: json['updatePeriod'] ?? 2000,
activeSensor: json['activeSensor'] ?? 'MQ-135',
includeLocationData: json['includeLocationData'] ?? true,
);
}
}
190 changes: 190 additions & 0 deletions lib/providers/ccs811_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pslab/communication/peripherals/i2c.dart';
import 'package:pslab/communication/science_lab.dart';
import 'package:pslab/others/logger_service.dart';
import '../communication/sensors/ccs811.dart';
import '../l10n/app_localizations.dart';
import '../models/chart_data_points.dart';
import 'locator.dart';

class CCS811Provider extends ChangeNotifier {
AppLocalizations get appLocalizations => getIt.get<AppLocalizations>();

CCS811? _ccs811;
Timer? _dataTimer;

int _eCO2 = 0;
int _tvoc = 0;

final List<ChartDataPoint> _eCO2Data = [];
final List<ChartDataPoint> _tvocData = [];

bool _isRunning = false;
bool _isLooping = false;
int _timegapMs = 1000;
int _numberOfReadings = 100;
int _collectedReadings = 0;

double _currentTime = 0.0;
static const int maxDataPoints = 1000;

int get eCO2 => _eCO2;
int get tvoc => _tvoc;

List<ChartDataPoint> get eCO2Data => List.unmodifiable(_eCO2Data);
List<ChartDataPoint> get tvocData => List.unmodifiable(_tvocData);

bool get isRunning => _isRunning;
bool get isLooping => _isLooping;
int get timegapMs => _timegapMs;
int get numberOfReadings => _numberOfReadings;
int get collectedReadings => _collectedReadings;

CCS811Provider();

Future<void> initializeSensors({
required Function(String) onError,
required I2C? i2c,
required ScienceLab? scienceLab,
}) async {
try {
if (i2c == null || scienceLab == null) {
onError(appLocalizations.pslabNotConnected);
logger.w(appLocalizations.notConnected);
return;
}

if (!scienceLab.isConnected()) {
onError(appLocalizations.pslabNotConnected);
logger.w(appLocalizations.notConnected);
return;
}

_ccs811 = await CCS811.create(i2c, scienceLab);
notifyListeners();
} catch (e) {
logger.e('Error initializing CCS811: $e');
}
}

void toggleDataCollection() {
if (_isRunning) {
_stopDataCollection();
} else {
_startDataCollection();
}
}

void _startDataCollection() {
if (_ccs811 == null) return;

_isRunning = true;
_collectedReadings = 0;

_dataTimer =
Timer.periodic(Duration(milliseconds: _timegapMs), (timer) async {
try {
await _fetchSensorData();
_collectedReadings++;

if (!_isLooping && _collectedReadings >= _numberOfReadings) {
_stopDataCollection();
}

if (_isLooping && _eCO2Data.length >= maxDataPoints) {
_removeOldestDataPoints();
}
} catch (e) {
logger.e('Error fetching sensor data: $e');
}
});
notifyListeners();
}

void _stopDataCollection() {
_isRunning = false;
_dataTimer?.cancel();
_dataTimer = null;
notifyListeners();
}

Future<void> _fetchSensorData() async {
if (_ccs811 == null) return;

try {
final rawData = await _ccs811!.getRawData();

_eCO2 = rawData['eCO2'] ?? 0;
_tvoc = rawData['TVOC'] ?? 0;

_currentTime += _timegapMs / 1000.0;

_addDataPoint(_eCO2Data, _eCO2.toDouble());
_addDataPoint(_tvocData, _tvoc.toDouble());

notifyListeners();
} catch (e) {
logger.e('Error in _fetchSensorData: $e');
rethrow;
}
}

void _addDataPoint(List<ChartDataPoint> dataList, double value) {
dataList.add(ChartDataPoint(_currentTime, value));
if (dataList.length > 50) {
dataList.removeAt(0);
}
}

void _removeOldestDataPoints() {
const keepPoints = 800;

if (_eCO2Data.length > keepPoints) {
final removeCount = _eCO2Data.length - keepPoints;
_eCO2Data.removeRange(0, removeCount);
_tvocData.removeRange(0, removeCount);
}
}

void toggleLooping() {
_isLooping = !_isLooping;
notifyListeners();
}

void setTimegap(int timegapMs) {
_timegapMs = timegapMs;

if (_isRunning) {
_stopDataCollection();
_startDataCollection();
}

notifyListeners();
}

void setNumberOfReadings(int numberOfReadings) {
_numberOfReadings = numberOfReadings;
notifyListeners();
}

void clearData() {
_eCO2Data.clear();
_tvocData.clear();
_eCO2 = 0;
_tvoc = 0;
_currentTime = 0.0;
_collectedReadings = 0;
notifyListeners();
}

bool get isCollectionComplete {
return !_isLooping && _collectedReadings >= _numberOfReadings;
}

@override
void dispose() {
_stopDataCollection();
super.dispose();
}
}
Loading
Loading