diff --git a/lib/providers/gyroscope_state_provider.dart b/lib/providers/gyroscope_state_provider.dart index 64f9e7196..542718292 100644 --- a/lib/providers/gyroscope_state_provider.dart +++ b/lib/providers/gyroscope_state_provider.dart @@ -4,6 +4,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/foundation.dart'; import 'package:sensors_plus/sensors_plus.dart'; import 'package:pslab/others/logger_service.dart'; +import 'package:intl/intl.dart'; class GyroscopeProvider extends ChangeNotifier { StreamSubscription? _gyroscopeSubscription; @@ -22,6 +23,9 @@ class GyroscopeProvider extends ChangeNotifier { double _yMin = 0, _yMax = 0; double _zMin = 0, _zMax = 0; + bool _isRecording = false; + List> _recordedData = []; + double get xValue => _gyroscopeEvent.x; double get yValue => _gyroscopeEvent.y; double get zValue => _gyroscopeEvent.z; @@ -34,6 +38,7 @@ class GyroscopeProvider extends ChangeNotifier { double get zMax => _zMax; bool get isListening => _gyroscopeSubscription != null; + bool get isRecording => _isRecording; void initializeSensors() { if (_gyroscopeSubscription != null) return; @@ -61,6 +66,20 @@ class GyroscopeProvider extends ChangeNotifier { final y = _gyroscopeEvent.y; final z = _gyroscopeEvent.z; + if (_isRecording) { + final now = DateTime.now(); + final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS'); + _recordedData.add([ + now.millisecondsSinceEpoch.toString(), + dateFormat.format(now), + x.toStringAsFixed(6), + y.toStringAsFixed(6), + z.toStringAsFixed(6), + 0, + 0 + ]); + } + _xData.add(x); _yData.add(y); _zData.add(z); @@ -88,6 +107,28 @@ class GyroscopeProvider extends ChangeNotifier { notifyListeners(); } + void startRecording() { + _isRecording = true; + _recordedData = [ + [ + 'Timestamp', + 'DateTime', + 'ReadingsX', + 'ReadingsY', + 'ReadingsZ', + 'Latitude', + 'Longitude' + ] + ]; + notifyListeners(); + } + + List> stopRecording() { + _isRecording = false; + notifyListeners(); + return _recordedData; + } + List getAxisData(String axis) { switch (axis) { case 'x': diff --git a/lib/view/compass_screen.dart b/lib/view/compass_screen.dart index 07cf252b8..86a0d01d6 100644 --- a/lib/view/compass_screen.dart +++ b/lib/view/compass_screen.dart @@ -180,15 +180,17 @@ class _CompassScreenContentState extends State { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Radio( - value: axis, + RadioGroup( groupValue: compassProvider.selectedAxis, onChanged: (String? value) { if (value != null) { compassProvider.onAxisSelected(value); } }, - activeColor: radioButtonActiveColor, + child: Radio( + value: axis, + activeColor: radioButtonActiveColor, + ), ), Text( label, diff --git a/lib/view/gyroscope_screen.dart b/lib/view/gyroscope_screen.dart index b3039c2f5..ce8a41588 100644 --- a/lib/view/gyroscope_screen.dart +++ b/lib/view/gyroscope_screen.dart @@ -7,7 +7,10 @@ import 'package:pslab/view/widgets/gyroscope_card.dart'; import 'package:pslab/view/widgets/common_scaffold_widget.dart'; import 'package:pslab/l10n/app_localizations.dart'; import 'package:pslab/providers/locator.dart'; +import 'package:pslab/others/csv_service.dart'; +import 'package:pslab/view/logged_data_screen.dart'; import '../theme/colors.dart'; +import '../constants.dart'; import 'gyroscope_config_screen.dart'; class GyroscopeScreen extends StatefulWidget { @@ -21,6 +24,27 @@ class _GyroscopeScreenState extends State { AppLocalizations appLocalizations = getIt.get(); bool _showGuide = false; static const imagePath = 'assets/images/gyroscope_axes_orientation.png'; + final CsvService _csvService = CsvService(); + late GyroscopeProvider _provider; + + @override + void initState() { + super.initState(); + _provider = GyroscopeProvider(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _provider.initializeSensors(); + } + }); + } + + @override + void dispose() { + _provider.disposeSensors(); + _provider.dispose(); + super.dispose(); + } + void _showInstrumentGuide() { setState(() { _showGuide = true; @@ -72,7 +96,7 @@ class _GyroscopeScreenState extends State { if (value != null) { switch (value) { case 'show_logged_data': - // TODO + _navigateToLoggedData(); break; case 'gyroscope_config': _navigateToConfig(); @@ -82,6 +106,19 @@ class _GyroscopeScreenState extends State { }); } + Future _navigateToLoggedData() async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LoggedDataScreen( + instrumentName: 'gyroscope', + appBarName: 'Gyroscope', + instrumentIcon: instrumentIcons[10], + ), + ), + ); + } + void _navigateToConfig() { Navigator.push( context, @@ -93,40 +130,122 @@ class _GyroscopeScreenState extends State { )); } + Future _toggleRecording() async { + if (_provider.isRecording) { + final data = _provider.stopRecording(); + await _showSaveFileDialog(data); + } else { + _provider.startRecording(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '${appLocalizations.recordingStarted}...', + style: TextStyle(color: snackBarContentColor), + ), + backgroundColor: snackBarBackgroundColor, + ), + ); + } + } + + Future _showSaveFileDialog(List> data) async { + final TextEditingController filenameController = TextEditingController(); + final String defaultFilename = ''; + filenameController.text = defaultFilename; + + final String? fileName = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(appLocalizations.saveRecording), + content: TextField( + controller: filenameController, + decoration: InputDecoration( + hintText: appLocalizations.enterFileName, + labelText: appLocalizations.fileName, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(appLocalizations.cancel.toUpperCase()), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context, filenameController.text); + }, + child: Text(appLocalizations.save), + ), + ], + ); + }, + ); + + if (fileName != null) { + _csvService.writeMetaData('gyroscope', data); + final file = await _csvService.saveCsvFile('gyroscope', fileName, data); + if (mounted) { + if (file != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '${appLocalizations.fileSaved}: ${file.path.split('/').last}', + style: TextStyle(color: snackBarContentColor), + ), + backgroundColor: snackBarBackgroundColor, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + appLocalizations.failedToSave, + style: TextStyle(color: snackBarContentColor), + ), + backgroundColor: snackBarBackgroundColor, + ), + ); + } + } + } + } + @override Widget build(BuildContext context) { - return MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (_) => GyroscopeProvider()..initializeSensors(), - ), - ], + return ChangeNotifierProvider.value( + value: _provider, child: Stack(children: [ - CommonScaffold( - title: appLocalizations.gyroscopeTitle, - onGuidePressed: _showInstrumentGuide, - onOptionsPressed: _showOptionsMenu, - body: SafeArea( - child: Column( - children: [ - Expanded( - child: GyroscopeCard( - color: xOrientationChartLineColor, - axis: appLocalizations.xAxis), + Consumer( + builder: (context, provider, child) { + return CommonScaffold( + title: appLocalizations.gyroscopeTitle, + onGuidePressed: _showInstrumentGuide, + onOptionsPressed: _showOptionsMenu, + onRecordPressed: _toggleRecording, + isRecording: provider.isRecording, + body: SafeArea( + child: Column( + children: [ + Expanded( + child: GyroscopeCard( + color: xOrientationChartLineColor, + axis: appLocalizations.xAxis), + ), + Expanded( + child: GyroscopeCard( + color: yOrientationChartLineColor, + axis: appLocalizations.yAxis), + ), + Expanded( + child: GyroscopeCard( + color: zOrientationChartLineColor, + axis: appLocalizations.zAxis), + ), + ], ), - Expanded( - child: GyroscopeCard( - color: yOrientationChartLineColor, - axis: appLocalizations.yAxis), - ), - Expanded( - child: GyroscopeCard( - color: zOrientationChartLineColor, - axis: appLocalizations.zAxis), - ), - ], - ), - ), + ), + ); + }, ), if (_showGuide) InstrumentOverviewDrawer( diff --git a/lib/view/logged_data_chart_screen.dart b/lib/view/logged_data_chart_screen.dart index a84cb6252..b321fffc6 100644 --- a/lib/view/logged_data_chart_screen.dart +++ b/lib/view/logged_data_chart_screen.dart @@ -13,6 +13,7 @@ class LoggedDataChartScreen extends StatefulWidget { final String yAxisLabel; final int xDataColumnIndex; final int yDataColumnIndex; + final String? instrumentName; const LoggedDataChartScreen({ super.key, @@ -22,6 +23,7 @@ class LoggedDataChartScreen extends StatefulWidget { this.yAxisLabel = 'Value', this.xDataColumnIndex = 0, this.yDataColumnIndex = 2, + this.instrumentName, }); @override @@ -30,6 +32,27 @@ class LoggedDataChartScreen extends StatefulWidget { class _LoggedDataChartScreenState extends State { AppLocalizations appLocalizations = getIt.get(); + String selectedAxis = 'x'; + bool get _shouldShowAxisSelector { + return widget.instrumentName?.toLowerCase() == 'gyroscope' || + widget.instrumentName?.toLowerCase() == 'accelerometer'; + } + + int _getYDataColumnIndex() { + if (_shouldShowAxisSelector) { + switch (selectedAxis) { + case 'x': + return 2; + case 'y': + return 3; + case 'z': + return 4; + default: + return 2; + } + } + return widget.yDataColumnIndex; + } @override void initState() { @@ -55,6 +78,14 @@ class _LoggedDataChartScreenState extends State { return interval > 0 ? interval : 1.0; } + double _getSafeYInterval(double minValue, double maxValue, + {int divisions = 5}) { + final double range = maxValue - minValue; + if (range <= 0) return 1.0; + final double interval = (range / divisions).ceilToDouble(); + return interval > 0 ? interval : 1.0; + } + double? _parseDouble(dynamic value) { if (value == null) return null; if (value is num) return value.toDouble(); @@ -68,8 +99,8 @@ class _LoggedDataChartScreenState extends State { return null; } - Widget _buildChart(double screenWidth, double maxY, double maxX, double minX, - double timeInterval, List spots) { + Widget _buildChart(double screenWidth, double minY, double maxY, double maxX, + double minX, double timeInterval, double yInterval, List spots) { final chartFontSize = screenWidth < 400 ? 8.0 : screenWidth < 600 @@ -77,7 +108,7 @@ class _LoggedDataChartScreenState extends State { : 10.0; final axisNameFontSize = screenWidth < 400 ? 9.0 : 10.0; final reservedSizeBottom = screenWidth < 400 ? 25.0 : 30.0; - final reservedSizeLeft = screenWidth < 400 ? 27.0 : 30.0; + final reservedSizeLeft = screenWidth < 400 ? 35.0 : 40.0; final reservedSizeRight = screenWidth < 400 ? 27.0 : 30.0; return Padding( @@ -125,7 +156,7 @@ class _LoggedDataChartScreenState extends State { return SideTitleWidget( meta: meta, child: Text( - value.toInt().toString(), + value.toStringAsFixed(1), style: TextStyle( color: blackTextColor, fontSize: chartFontSize, @@ -133,7 +164,7 @@ class _LoggedDataChartScreenState extends State { ), ); }, - interval: maxY > 0 ? (maxY / 5).ceilToDouble() : 10, + interval: yInterval, ), ), rightTitles: AxisTitles( @@ -145,7 +176,7 @@ class _LoggedDataChartScreenState extends State { show: true, drawHorizontalLine: true, drawVerticalLine: true, - horizontalInterval: maxY > 0 ? (maxY / 5).ceilToDouble() : 10, + horizontalInterval: yInterval, verticalInterval: timeInterval, ), borderData: FlBorderData( @@ -157,8 +188,8 @@ class _LoggedDataChartScreenState extends State { right: BorderSide(color: chartBorderColor), ), ), - minY: 0, - maxY: maxY > 0 ? (maxY * 1.1) : 100, + minY: minY, + maxY: maxY, maxX: maxX > 0 ? maxX : 10, minX: minX, clipData: const FlClipData.all(), @@ -181,7 +212,8 @@ class _LoggedDataChartScreenState extends State { @override Widget build(BuildContext context) { final List spots = []; - double maxY = 0; + double maxY = double.negativeInfinity; + double minY = double.infinity; double maxX = 0; double minX = 0; double? startTime; @@ -191,7 +223,7 @@ class _LoggedDataChartScreenState extends State { if (row.length > widget.xDataColumnIndex && row.length > widget.yDataColumnIndex) { final xValue = _parseDouble(row[widget.xDataColumnIndex]); - final yValue = _parseDouble(row[widget.yDataColumnIndex]); + final yValue = _parseDouble(row[_getYDataColumnIndex()]); if (xValue != null && yValue != null) { if (startTime == null) { @@ -202,18 +234,71 @@ class _LoggedDataChartScreenState extends State { final relativeTime = ((xValue - startTime) / 1000.0); spots.add(FlSpot(relativeTime, yValue)); + if (yValue > maxY) maxY = yValue; + if (yValue < minY) minY = yValue; + if (relativeTime > maxX) maxX = relativeTime; } } } + if (spots.isEmpty) { + minY = 0; + maxY = 100; + } else if (minY == maxY) { + final padding = minY.abs() * 0.1; + if (padding == 0) { + minY = -1; + maxY = 1; + } else { + minY -= padding; + maxY += padding; + } + } else { + final range = maxY - minY; + final padding = range * 0.1; + minY -= padding; + maxY += padding; + } + final screenWidth = MediaQuery.of(context).size.width; final timeInterval = _getSafeInterval(maxX, divisions: 10); + final yInterval = _getSafeYInterval(minY, maxY, divisions: 5); return Scaffold( backgroundColor: scaffoldBackgroundColor, appBar: AppBar( + actions: _shouldShowAxisSelector + ? [ + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: DropdownButton( + value: selectedAxis, + dropdownColor: primaryRed, + underline: Container(), + icon: + Icon(Icons.arrow_drop_down, color: appBarContentColor), + items: ['x', 'y', 'z'].map((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value.toUpperCase(), + style: TextStyle(color: appBarContentColor), + ), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + setState(() { + selectedAxis = newValue; + }); + } + }, + ), + ), + ] + : null, title: Text( widget.fileName, style: TextStyle(color: appBarContentColor, fontSize: 15), @@ -240,8 +325,8 @@ class _LoggedDataChartScreenState extends State { MediaQuery.of(context).padding.top - MediaQuery.of(context).padding.bottom - 48, - child: _buildChart( - screenWidth, maxY, maxX, minX, timeInterval, spots), + child: _buildChart(screenWidth, minY, maxY, maxX, minX, + timeInterval, yInterval, spots), ), ), ), diff --git a/lib/view/logged_data_screen.dart b/lib/view/logged_data_screen.dart index 00256f285..f33387796 100644 --- a/lib/view/logged_data_screen.dart +++ b/lib/view/logged_data_screen.dart @@ -126,14 +126,14 @@ class _LoggedDataScreenState extends State { return { 'xAxisLabel': appLocalizations.timeAxisLabel, 'yAxisLabel': appLocalizations.atm, - 'xDataColumnIndex': 1, + 'xDataColumnIndex': 0, 'yDataColumnIndex': 2, }; default: return { 'xAxisLabel': appLocalizations.timeAxisLabel, 'yAxisLabel': 'Value', - 'xDataColumnIndex': 1, + 'xDataColumnIndex': 0, 'yDataColumnIndex': 2, }; } @@ -156,6 +156,7 @@ class _LoggedDataScreenState extends State { yAxisLabel: config['yAxisLabel'], xDataColumnIndex: config['xDataColumnIndex'], yDataColumnIndex: config['yDataColumnIndex'], + instrumentName: widget.instrumentName, ), ), ); @@ -180,6 +181,7 @@ class _LoggedDataScreenState extends State { yAxisLabel: config['yAxisLabel'], xDataColumnIndex: config['xDataColumnIndex'], yDataColumnIndex: config['yDataColumnIndex'], + instrumentName: widget.instrumentName, ), ), );