Skip to content

Commit 8eeb04c

Browse files
authored
feat: Implement CCS811 sensor (#3363)
1 parent 210eb4b commit 8eeb04c

6 files changed

Lines changed: 610 additions & 1 deletion

File tree

assets/images/ccs811.jpg

81.4 KB
Loading
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import 'dart:async';
2+
import 'package:pslab/communication/peripherals/i2c.dart';
3+
import 'package:pslab/communication/science_lab.dart';
4+
import 'package:pslab/others/logger_service.dart';
5+
6+
class CCS811 {
7+
static const String tag = "CCS811";
8+
static const int address = 0x5A;
9+
10+
static const int algResultData = 0x02;
11+
static const int hwId = 0x20;
12+
static const int fwBootVersion = 0x23;
13+
static const int fwAppVersion = 0x24;
14+
static const int measMode = 0x01;
15+
static const int hwVersion = 0x21;
16+
static const int appStart = 0xF4;
17+
18+
static const int driveMode1Sec = 0x01;
19+
20+
final I2C i2c;
21+
22+
CCS811._(this.i2c);
23+
24+
static Future<CCS811> create(I2C i2c, ScienceLab scienceLab) async {
25+
final ccs811 = CCS811._(i2c);
26+
if (scienceLab.isConnected()) {
27+
await ccs811._initialize();
28+
}
29+
return ccs811;
30+
}
31+
32+
Future<void> _initialize() async {
33+
try {
34+
await _fetchID();
35+
await _appStart();
36+
await Future.delayed(const Duration(milliseconds: 100));
37+
await _disableInterrupt();
38+
await _setMeasMode();
39+
} catch (e) {
40+
logger.e("$tag Error initializing: $e");
41+
rethrow;
42+
}
43+
}
44+
45+
Future<void> _setMeasMode() async {
46+
int config = (1 << 2) | (driveMode1Sec << 4);
47+
await i2c.write(address, [config], measMode);
48+
}
49+
50+
Future<void> _disableInterrupt() async {
51+
int config = (1 << 2) | (3 << 4);
52+
await i2c.write(address, [config], measMode);
53+
}
54+
55+
Future<void> _fetchID() async {
56+
try {
57+
List<int> hwIdData = await i2c.readBulk(address, hwId, 1);
58+
await Future.delayed(const Duration(milliseconds: 20));
59+
60+
List<int> hwVerData = await i2c.readBulk(address, hwVersion, 1);
61+
await Future.delayed(const Duration(milliseconds: 20));
62+
63+
List<int> bootVerData = await i2c.readBulk(address, fwBootVersion, 2);
64+
await Future.delayed(const Duration(milliseconds: 20));
65+
66+
List<int> appVerData = await i2c.readBulk(address, fwAppVersion, 2);
67+
await Future.delayed(const Duration(milliseconds: 20));
68+
69+
if (hwIdData.isNotEmpty) {
70+
logger.d("$tag Hardware ID: ${hwIdData[0] & 0xFF}");
71+
}
72+
if (hwVerData.isNotEmpty) {
73+
logger.d("$tag Hardware Version: ${hwVerData[0] & 0xFF}");
74+
}
75+
if (bootVerData.length >= 2) {
76+
logger
77+
.d("$tag Boot Version: ${(bootVerData[0] << 8) | bootVerData[1]}");
78+
}
79+
if (appVerData.length >= 2) {
80+
logger.d("$tag App Version: ${(appVerData[0] << 8) | appVerData[1]}");
81+
}
82+
} catch (e) {
83+
logger.e("$tag Error fetching IDs: $e");
84+
}
85+
}
86+
87+
Future<void> _appStart() async {
88+
await i2c.write(address, [], appStart);
89+
}
90+
91+
String _decodeError(int error) {
92+
String e = "";
93+
if ((error & 1) > 0) e += ", Invalid register address ID";
94+
if ((error & (1 << 1)) > 0) e += ", Invalid mailbox ID";
95+
if ((error & (1 << 2)) > 0) e += ", Unsupported mode to MEAS_MODE";
96+
if ((error & (1 << 3)) > 0) {
97+
e += ", Resistance measurement max range reached";
98+
}
99+
if ((error & (1 << 4)) > 0) e += ", Heater current out of range";
100+
if ((error & (1 << 5)) > 0) e += ", Heater voltage applied incorrectly";
101+
102+
return e.isNotEmpty ? "Error: ${e.substring(2)}" : "Unknown error";
103+
}
104+
105+
Future<Map<String, int>> getRawData() async {
106+
try {
107+
List<int> data = await i2c.readBulk(address, algResultData, 8);
108+
if (data.length < 8) {
109+
throw Exception("Expected 8 bytes but got ${data.length}");
110+
}
111+
112+
int eCO2 = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF);
113+
int tvoc = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF);
114+
int errorId = data[5] & 0xFF;
115+
116+
if (errorId > 0) {
117+
logger.e("$tag ${_decodeError(errorId)}");
118+
}
119+
120+
return {
121+
'eCO2': eCO2,
122+
'TVOC': tvoc,
123+
};
124+
} catch (e) {
125+
logger.e("$tag Error getting raw data: $e");
126+
rethrow;
127+
}
128+
}
129+
}

lib/l10n/app_en.arb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,5 +763,14 @@
763763
"sensorDescVL53L0X": "Time-of-Flight (ToF) Laser Distance",
764764
"mightNotConnected" : "might not be connected.",
765765
"hmc5883l" : "HMC5883L",
766-
"gauss" : "Gauss"
766+
"gauss" : "Gauss",
767+
"i2cError" : "Check I2C wiring.",
768+
"ccs811" : "CCS811",
769+
"eco2" : "eCO2",
770+
"tvoc" : "TVOC",
771+
"ppb" : "ppb",
772+
"concentrationPpm" : "Concentration (PPM)",
773+
"gasSensorConfig" : "Gas Sensor Configurations",
774+
"gasSensorPeriodHint" : "Enter update period (100 - 5000 ms)",
775+
"mq135" : "MQ-135"
767776
}

lib/providers/ccs811_provider.dart

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import 'dart:async';
2+
import 'package:flutter/material.dart';
3+
import 'package:pslab/communication/peripherals/i2c.dart';
4+
import 'package:pslab/communication/science_lab.dart';
5+
import 'package:pslab/others/logger_service.dart';
6+
import '../communication/sensors/ccs811.dart';
7+
import '../l10n/app_localizations.dart';
8+
import '../models/chart_data_points.dart';
9+
import 'locator.dart';
10+
11+
class CCS811Provider extends ChangeNotifier {
12+
AppLocalizations get appLocalizations => getIt.get<AppLocalizations>();
13+
14+
CCS811? _ccs811;
15+
Timer? _dataTimer;
16+
17+
int _eCO2 = 0;
18+
int _tvoc = 0;
19+
20+
final List<ChartDataPoint> _eCO2Data = [];
21+
final List<ChartDataPoint> _tvocData = [];
22+
23+
bool _isRunning = false;
24+
bool _isLooping = false;
25+
int _timegapMs = 1000;
26+
int _numberOfReadings = 100;
27+
int _collectedReadings = 0;
28+
29+
double _currentTime = 0.0;
30+
static const int maxDataPoints = 1000;
31+
32+
int get eCO2 => _eCO2;
33+
int get tvoc => _tvoc;
34+
35+
List<ChartDataPoint> get eCO2Data => List.unmodifiable(_eCO2Data);
36+
List<ChartDataPoint> get tvocData => List.unmodifiable(_tvocData);
37+
38+
bool get isRunning => _isRunning;
39+
bool get isLooping => _isLooping;
40+
int get timegapMs => _timegapMs;
41+
int get numberOfReadings => _numberOfReadings;
42+
int get collectedReadings => _collectedReadings;
43+
44+
CCS811Provider();
45+
46+
Future<void> initializeSensors({
47+
required Function(String) onError,
48+
required I2C? i2c,
49+
required ScienceLab? scienceLab,
50+
}) async {
51+
try {
52+
if (i2c == null || scienceLab == null) {
53+
onError(appLocalizations.pslabNotConnected);
54+
logger.w(appLocalizations.notConnected);
55+
return;
56+
}
57+
58+
if (!scienceLab.isConnected()) {
59+
onError(appLocalizations.pslabNotConnected);
60+
logger.w(appLocalizations.notConnected);
61+
return;
62+
}
63+
64+
_ccs811 = await CCS811.create(i2c, scienceLab);
65+
notifyListeners();
66+
} catch (e) {
67+
logger.e('Error initializing CCS811: $e');
68+
}
69+
}
70+
71+
void toggleDataCollection() {
72+
if (_isRunning) {
73+
_stopDataCollection();
74+
} else {
75+
_startDataCollection();
76+
}
77+
}
78+
79+
void _startDataCollection() {
80+
if (_ccs811 == null) return;
81+
82+
_isRunning = true;
83+
_collectedReadings = 0;
84+
85+
_dataTimer =
86+
Timer.periodic(Duration(milliseconds: _timegapMs), (timer) async {
87+
try {
88+
await _fetchSensorData();
89+
_collectedReadings++;
90+
91+
if (!_isLooping && _collectedReadings >= _numberOfReadings) {
92+
_stopDataCollection();
93+
}
94+
95+
if (_isLooping && _eCO2Data.length >= maxDataPoints) {
96+
_removeOldestDataPoints();
97+
}
98+
} catch (e) {
99+
logger.e('Error fetching sensor data: $e');
100+
}
101+
});
102+
notifyListeners();
103+
}
104+
105+
void _stopDataCollection() {
106+
_isRunning = false;
107+
_dataTimer?.cancel();
108+
_dataTimer = null;
109+
notifyListeners();
110+
}
111+
112+
Future<void> _fetchSensorData() async {
113+
if (_ccs811 == null) return;
114+
115+
try {
116+
final rawData = await _ccs811!.getRawData();
117+
118+
_eCO2 = rawData['eCO2'] ?? 0;
119+
_tvoc = rawData['TVOC'] ?? 0;
120+
121+
_currentTime += _timegapMs / 1000.0;
122+
123+
_addDataPoint(_eCO2Data, _eCO2.toDouble());
124+
_addDataPoint(_tvocData, _tvoc.toDouble());
125+
126+
notifyListeners();
127+
} catch (e) {
128+
logger.e('Error in _fetchSensorData: $e');
129+
rethrow;
130+
}
131+
}
132+
133+
void _addDataPoint(List<ChartDataPoint> dataList, double value) {
134+
dataList.add(ChartDataPoint(_currentTime, value));
135+
if (dataList.length > 50) {
136+
dataList.removeAt(0);
137+
}
138+
}
139+
140+
void _removeOldestDataPoints() {
141+
const keepPoints = 800;
142+
143+
if (_eCO2Data.length > keepPoints) {
144+
final removeCount = _eCO2Data.length - keepPoints;
145+
_eCO2Data.removeRange(0, removeCount);
146+
_tvocData.removeRange(0, removeCount);
147+
}
148+
}
149+
150+
void toggleLooping() {
151+
_isLooping = !_isLooping;
152+
notifyListeners();
153+
}
154+
155+
void setTimegap(int timegapMs) {
156+
_timegapMs = timegapMs;
157+
158+
if (_isRunning) {
159+
_stopDataCollection();
160+
_startDataCollection();
161+
}
162+
163+
notifyListeners();
164+
}
165+
166+
void setNumberOfReadings(int numberOfReadings) {
167+
_numberOfReadings = numberOfReadings;
168+
notifyListeners();
169+
}
170+
171+
void clearData() {
172+
_eCO2Data.clear();
173+
_tvocData.clear();
174+
_eCO2 = 0;
175+
_tvoc = 0;
176+
_currentTime = 0.0;
177+
_collectedReadings = 0;
178+
notifyListeners();
179+
}
180+
181+
bool get isCollectionComplete {
182+
return !_isLooping && _collectedReadings >= _numberOfReadings;
183+
}
184+
185+
@override
186+
void dispose() {
187+
_stopDataCollection();
188+
super.dispose();
189+
}
190+
}

0 commit comments

Comments
 (0)