Skip to content

Commit 66bb5e7

Browse files
feat: added csv functionalities in soundmeter screen. (#2796)
Co-authored-by: Marc Nause <[email protected]>
1 parent d750db6 commit 66bb5e7

8 files changed

+351
-91
lines changed

lib/l10n/app_en.arb

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,43 @@
332332
"sharingMessage" : "Sharing PSLab Data",
333333
"delete" : "Delete",
334334
"deleteHint": "Are you sure you want to delete this file?",
335+
"deleteFile" : "Delete File",
336+
"deleteAllData" : "Delete All Data",
337+
"deleteCautionMessage" : "Are you sure you want to delete all logged data for this instrument?",
338+
"deleteAll" : "Delete All",
339+
"noLoggedData" : "No logged data found.",
340+
"importLog" : "Import Log",
341+
"failedToSave" : "Failed to save file. No data was recorded.",
342+
"fileSaved" : "File saved",
343+
"save" : "Save",
344+
"enterFileName" : "Enter filename (leave empty for auto-generated name)",
345+
"fileName" : "Filename",
346+
"saveRecording" : "Save Recording",
347+
"recordingStarted" : "Recording started",
348+
"noValidData" : "No valid data to display.",
349+
"csvPickingError" : "Error picking or reading CSV file",
350+
"csvReadingError" : "Error reading CSV from file",
351+
"sharingError" : "Error sharing file",
352+
"csvGettingError" : "Error getting saved files",
353+
"unsupportedPlatform" : "Unsupported platform",
354+
"noDataRecorded" : "No data recorded to save for",
355+
"csvFileSaved" : "CSV file saved at",
356+
"csvSavingError" : "Error saving CSV file",
357+
"csvDeletingError" : "Error deleting file",
358+
"fileDeleted" : "File deleted",
359+
"soundmeterConfig" : "Soundmeter Configurations",
360+
"barometerConfig" : "Barometer Configurations",
361+
"baroUpdatePeriodHint" : "Please provide time interval at which data will be updated (100 ms to 2000 ms)",
362+
"barometerHighLimitHint" : "Please provide the maximum limit of lux value to be recorded (0 atm to 1.10 atm)",
363+
"gyroscopeConfigurations" : "Gyroscope Configurations",
364+
"gyroscopeHighLimitHint" : "Please provide the maximum limit of lux value to be recorded (0 rad/s to 1000 rad/s)",
365+
"accelerometerConfigurations" : "Accelerometer Configurations",
366+
"accelerometerUpdatePeriodHint" : "Please provide time interval at which data will be updated",
367+
"accelerometerHighLimitHint" : "Please provide the maximum limit of lux value to be recorded",
368+
"soundmeterSnackBarMessage" : "Unable to access sound sensor",
369+
"dangerous" : "Dangerous",
370+
"roboticArmIntro": "• A robotic arm is a programmable mechanical device that mimics the movement of a human arm.\n• It uses servo motors to control its motion, and these motors are operated using PWM signals.\n• The PSLab provides four PWM square wave generators (SQ1, SQ2, SQ3, SQ4), allowing control of up to four servo motors and enabling a robotic arm with up to four degrees of freedom.",
371+
"roboticArmConnection": "• In the above figure, SQ1 is connected to the signal pin of the first servo motor. The servo's GND pin is connected to both the PSLab’s GND and the external power supply GND, while the VCC pin is connected to the external power supply VCC.\n• Similarly, connect the remaining servos to SQ2, SQ3, and SQ4 along with their respective GND and power supply connections.\n• Once connected, each servo can be controlled using either circular sliders for manual control or a timeline-based sequence for automated movement.",
335372
"documentationLink" : "https://docs.pslab.io/",
336373
"documentationError" : "Could not open the documentation link",
337374
"deleteFile": "Delete File",
@@ -391,4 +428,4 @@
391428
"time" : "Time",
392429
"notAvailable" : "N/A",
393430
"estimated" : "Estimated"
394-
}
431+
}

lib/l10n/app_localizations.dart

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,18 +2086,6 @@ abstract class AppLocalizations {
20862086
/// **'Are you sure you want to delete this file?'**
20872087
String get deleteHint;
20882088

2089-
/// No description provided for @documentationLink.
2090-
///
2091-
/// In en, this message translates to:
2092-
/// **'https://docs.pslab.io/'**
2093-
String get documentationLink;
2094-
2095-
/// No description provided for @documentationError.
2096-
///
2097-
/// In en, this message translates to:
2098-
/// **'Could not open the documentation link'**
2099-
String get documentationError;
2100-
21012089
/// No description provided for @deleteFile.
21022090
///
21032091
/// In en, this message translates to:
@@ -2296,6 +2284,18 @@ abstract class AppLocalizations {
22962284
/// **'Please provide the maximum limit of lux value to be recorded'**
22972285
String get accelerometerHighLimitHint;
22982286

2287+
/// No description provided for @soundmeterSnackBarMessage.
2288+
///
2289+
/// In en, this message translates to:
2290+
/// **'Unable to access sound sensor'**
2291+
String get soundmeterSnackBarMessage;
2292+
2293+
/// No description provided for @dangerous.
2294+
///
2295+
/// In en, this message translates to:
2296+
/// **'Dangerous'**
2297+
String get dangerous;
2298+
22992299
/// No description provided for @roboticArmIntro.
23002300
///
23012301
/// In en, this message translates to:
@@ -2308,6 +2308,18 @@ abstract class AppLocalizations {
23082308
/// **'• In the above figure, SQ1 is connected to the signal pin of the first servo motor. The servo\'s GND pin is connected to both the PSLab’s GND and the external power supply GND, while the VCC pin is connected to the external power supply VCC.\n• Similarly, connect the remaining servos to SQ2, SQ3, and SQ4 along with their respective GND and power supply connections.\n• Once connected, each servo can be controlled using either circular sliders for manual control or a timeline-based sequence for automated movement.'**
23092309
String get roboticArmConnection;
23102310

2311+
/// No description provided for @documentationLink.
2312+
///
2313+
/// In en, this message translates to:
2314+
/// **'https://docs.pslab.io/'**
2315+
String get documentationLink;
2316+
2317+
/// No description provided for @documentationError.
2318+
///
2319+
/// In en, this message translates to:
2320+
/// **'Could not open the documentation link'**
2321+
String get documentationError;
2322+
23112323
/// No description provided for @autoscan.
23122324
///
23132325
/// In en, this message translates to:

lib/l10n/app_localizations_en.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,12 +1067,6 @@ class AppLocalizationsEn extends AppLocalizations {
10671067
@override
10681068
String get deleteHint => 'Are you sure you want to delete this file?';
10691069

1070-
@override
1071-
String get documentationLink => 'https://docs.pslab.io/';
1072-
1073-
@override
1074-
String get documentationError => 'Could not open the documentation link';
1075-
10761070
@override
10771071
String get deleteFile => 'Delete File';
10781072

@@ -1179,6 +1173,12 @@ class AppLocalizationsEn extends AppLocalizations {
11791173
String get accelerometerHighLimitHint =>
11801174
'Please provide the maximum limit of lux value to be recorded';
11811175

1176+
@override
1177+
String get soundmeterSnackBarMessage => 'Unable to access sound sensor';
1178+
1179+
@override
1180+
String get dangerous => 'Dangerous';
1181+
11821182
@override
11831183
String get roboticArmIntro =>
11841184
'• A robotic arm is a programmable mechanical device that mimics the movement of a human arm.\n• It uses servo motors to control its motion, and these motors are operated using PWM signals.\n• The PSLab provides four PWM square wave generators (SQ1, SQ2, SQ3, SQ4), allowing control of up to four servo motors and enabling a robotic arm with up to four degrees of freedom.';
@@ -1187,6 +1187,12 @@ class AppLocalizationsEn extends AppLocalizations {
11871187
String get roboticArmConnection =>
11881188
'• In the above figure, SQ1 is connected to the signal pin of the first servo motor. The servo\'s GND pin is connected to both the PSLab’s GND and the external power supply GND, while the VCC pin is connected to the external power supply VCC.\n• Similarly, connect the remaining servos to SQ2, SQ3, and SQ4 along with their respective GND and power supply connections.\n• Once connected, each servo can be controlled using either circular sliders for manual control or a timeline-based sequence for automated movement.';
11891189

1190+
@override
1191+
String get documentationLink => 'https://docs.pslab.io/';
1192+
1193+
@override
1194+
String get documentationError => 'Could not open the documentation link';
1195+
11901196
@override
11911197
String get autoscan => 'Autoscan';
11921198

lib/providers/luxmeter_state_provider.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ class LuxMeterStateProvider extends ChangeNotifier {
2828
bool _sensorAvailable = false;
2929
bool _isRecording = false;
3030
List<List<dynamic>> _recordedData = [];
31-
double _recordingStartTime = 0.0;
3231
bool get isRecording => _isRecording;
3332

3433
LuxMeterConfigProvider? _configProvider;
@@ -112,13 +111,14 @@ class LuxMeterStateProvider extends ChangeNotifier {
112111
final time = _currentTime;
113112
if (lux != null) {
114113
if (_isRecording) {
115-
final relativeTime = time - _recordingStartTime;
116114
final now = DateTime.now();
117115
final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS');
118116
_recordedData.add([
117+
now.millisecondsSinceEpoch.toString(),
119118
dateFormat.format(now),
120-
relativeTime.toStringAsFixed(2),
121119
lux.toStringAsFixed(2),
120+
0,
121+
0
122122
]);
123123
}
124124

@@ -146,8 +146,9 @@ class LuxMeterStateProvider extends ChangeNotifier {
146146

147147
void startRecording() {
148148
_isRecording = true;
149-
_recordingStartTime = _currentTime;
150-
_recordedData = [];
149+
_recordedData = [
150+
['Timestamp', 'DateTime', 'Readings', 'Latitude', 'Longitude']
151+
];
151152
notifyListeners();
152153
}
153154

lib/providers/soundmeter_state_provider.dart

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22
import 'dart:math';
33
import 'package:fl_chart/fl_chart.dart';
4+
import 'package:intl/intl.dart';
45
import 'package:pslab/l10n/app_localizations.dart';
56
import 'package:pslab/others/logger_service.dart';
67
import 'package:flutter/foundation.dart';
@@ -18,13 +19,20 @@ class SoundMeterStateProvider extends ChangeNotifier {
1819
AudioJack? _audioJack;
1920
double _startTime = 0;
2021
double _currentTime = 0;
21-
final int _maxLength = 50;
22+
final int _chartMaxLength = 50;
2223
double _dbMin = 0;
2324
double _dbMax = 0;
2425
double _dbSum = 0;
2526
int _dataCount = 0;
27+
bool _isRecording = false;
28+
List<List<dynamic>> _recordedData = [];
29+
bool get isRecording => _isRecording;
30+
31+
Function(String)? onSensorError;
32+
33+
void initializeSensors({Function(String)? onError}) async {
34+
onSensorError = onError;
2635

27-
void initializeSensors() async {
2836
try {
2937
_audioJack = AudioJack();
3038
await _audioJack!.initialize();
@@ -50,9 +58,15 @@ class SoundMeterStateProvider extends ChangeNotifier {
5058
});
5159
} catch (e) {
5260
logger.e("${appLocalizations.soundMeterInitialError} $e");
61+
_handleSensorError(e);
5362
}
5463
}
5564

65+
void _handleSensorError(dynamic error) {
66+
onSensorError?.call(appLocalizations.soundmeterSnackBarMessage);
67+
logger.e("${appLocalizations.soundMeterInitialError} $error");
68+
}
69+
5670
double _calculateDecibels(List<double> audioData) {
5771
if (audioData.isEmpty) return 0.0;
5872

@@ -87,11 +101,22 @@ class SoundMeterStateProvider extends ChangeNotifier {
87101
void _updateData() {
88102
final db = _currentDb;
89103
final time = _currentTime;
104+
if (_isRecording) {
105+
final now = DateTime.now();
106+
final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS');
107+
_recordedData.add([
108+
now.millisecondsSinceEpoch.toString(),
109+
dateFormat.format(now),
110+
db.toStringAsFixed(2),
111+
0,
112+
0
113+
]);
114+
}
90115
_dbData.add(db);
91116
_timeData.add(time);
92117
_dbSum += db;
93118
_dataCount++;
94-
if (_dbData.length > _maxLength) {
119+
if (_dbData.length > _chartMaxLength) {
95120
final removedValue = _dbData.removeAt(0);
96121
_timeData.removeAt(0);
97122
_dbSum -= removedValue;
@@ -108,6 +133,20 @@ class SoundMeterStateProvider extends ChangeNotifier {
108133
notifyListeners();
109134
}
110135

136+
void startRecording() {
137+
_isRecording = true;
138+
_recordedData = [
139+
['Timestamp', 'DateTime', 'Readings', 'Latitude', 'Longitude']
140+
];
141+
notifyListeners();
142+
}
143+
144+
List<List<dynamic>> stopRecording() {
145+
_isRecording = false;
146+
notifyListeners();
147+
return _recordedData;
148+
}
149+
111150
double getCurrentDb() => _currentDb;
112151
double getMinDb() => _dbMin;
113152
double getMaxDb() => _dbMax;

lib/view/logged_data_chart_screen.dart

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class LoggedDataChartScreen extends StatefulWidget {
2020
required this.fileName,
2121
this.xAxisLabel = 'Time (s)',
2222
this.yAxisLabel = 'Value',
23-
this.xDataColumnIndex = 1,
23+
this.xDataColumnIndex = 0,
2424
this.yDataColumnIndex = 2,
2525
});
2626

@@ -105,18 +105,7 @@ class _LoggedDataChartScreenState extends State<LoggedDataChartScreen> {
105105
sideTitles: SideTitles(
106106
showTitles: true,
107107
reservedSize: reservedSizeBottom,
108-
getTitlesWidget: (value, meta) {
109-
return SideTitleWidget(
110-
meta: meta,
111-
child: Text(
112-
value.toStringAsFixed(1),
113-
style: TextStyle(
114-
color: blackTextColor,
115-
fontSize: chartFontSize,
116-
),
117-
),
118-
);
119-
},
108+
getTitlesWidget: _sideTitleWidgets,
120109
interval: timeInterval,
121110
),
122111
),
@@ -195,6 +184,7 @@ class _LoggedDataChartScreenState extends State<LoggedDataChartScreen> {
195184
double maxY = 0;
196185
double maxX = 0;
197186
double minX = 0;
187+
double? startTime;
198188

199189
for (int i = 1; i < widget.data.length; i++) {
200190
final row = widget.data[i];
@@ -204,10 +194,16 @@ class _LoggedDataChartScreenState extends State<LoggedDataChartScreen> {
204194
final yValue = _parseDouble(row[widget.yDataColumnIndex]);
205195

206196
if (xValue != null && yValue != null) {
207-
spots.add(FlSpot(xValue, yValue));
197+
if (startTime == null) {
198+
startTime = xValue;
199+
minX = 0;
200+
}
201+
202+
final relativeTime = ((xValue - startTime) / 1000.0);
203+
204+
spots.add(FlSpot(relativeTime, yValue));
208205
if (yValue > maxY) maxY = yValue;
209-
if (xValue > maxX) maxX = xValue;
210-
if (spots.length == 1 || xValue < minX) minX = xValue;
206+
if (relativeTime > maxX) maxX = relativeTime;
211207
}
212208
}
213209
}
@@ -252,4 +248,39 @@ class _LoggedDataChartScreenState extends State<LoggedDataChartScreen> {
252248
),
253249
);
254250
}
251+
252+
Widget _sideTitleWidgets(double value, TitleMeta meta) {
253+
final screenWidth = MediaQuery.of(context).size.width;
254+
final fontSize = screenWidth < 400
255+
? 7.0
256+
: screenWidth < 600
257+
? 8.0
258+
: 9.0;
259+
final style = TextStyle(
260+
color: blackTextColor,
261+
fontSize: fontSize,
262+
);
263+
264+
String timeText;
265+
if (value < 60) {
266+
timeText = '${value.toInt()}s';
267+
} else if (value < 3600) {
268+
int minutes = (value / 60).floor();
269+
int seconds = (value % 60).toInt();
270+
timeText = '${minutes}m${seconds}s';
271+
} else {
272+
int hours = (value / 3600).floor();
273+
int minutes = ((value % 3600) / 60).floor();
274+
timeText = '${hours}h${minutes}m';
275+
}
276+
277+
return SideTitleWidget(
278+
meta: meta,
279+
child: Text(
280+
maxLines: 1,
281+
timeText,
282+
style: style,
283+
),
284+
);
285+
}
255286
}

lib/view/logged_data_screen.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,14 @@ class _LoggedDataScreenState extends State<LoggedDataScreen> {
112112
return {
113113
'xAxisLabel': appLocalizations.timeAxisLabel,
114114
'yAxisLabel': appLocalizations.lx,
115-
'xDataColumnIndex': 1,
115+
'xDataColumnIndex': 0,
116116
'yDataColumnIndex': 2,
117117
};
118118
case 'soundmeter':
119119
return {
120120
'xAxisLabel': appLocalizations.timeAxisLabel,
121121
'yAxisLabel': appLocalizations.db,
122-
'xDataColumnIndex': 1,
122+
'xDataColumnIndex': 0,
123123
'yDataColumnIndex': 2,
124124
};
125125
case 'barometer':

0 commit comments

Comments
 (0)