diff --git a/lib/constants.dart b/lib/constants.dart index c4cebf642..323a4c469 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -141,3 +141,6 @@ String timeAxisLabel = 'Time(s)'; String accelerationAxisLabel = 'm/s²'; String minValue = 'Min: '; String maxValue = 'Max: '; +String gyroscopeTitle = "Gyroscope"; +String gyroscopeAxisLabel = 'rad/s'; +String noData = 'No data available'; diff --git a/lib/main.dart b/lib/main.dart index 79ef73835..47decd45a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:pslab/providers/locator.dart'; import 'package:pslab/view/accelerometer_screen.dart'; import 'package:pslab/view/connect_device_screen.dart'; import 'package:pslab/view/faq_screen.dart'; +import 'package:pslab/view/gyroscope_screen.dart'; import 'package:pslab/view/instruments_screen.dart'; import 'package:pslab/view/oscilloscope_screen.dart'; import 'package:pslab/view/settings_screen.dart'; @@ -51,6 +52,7 @@ class MyApp extends StatelessWidget { '/aboutUs': (context) => const AboutUsScreen(), '/softwareLicenses': (context) => const SoftwareLicensesScreen(), '/accelerometer': (context) => const AccelerometerScreen(), + '/gyroscope': (context) => const GyroscopeScreen(), }, ); } diff --git a/lib/providers/gyroscope_state_provider.dart b/lib/providers/gyroscope_state_provider.dart new file mode 100644 index 000000000..64f9e7196 --- /dev/null +++ b/lib/providers/gyroscope_state_provider.dart @@ -0,0 +1,161 @@ +import 'dart:async'; +import 'dart:math'; +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'; + +class GyroscopeProvider extends ChangeNotifier { + StreamSubscription? _gyroscopeSubscription; + GyroscopeEvent _gyroscopeEvent = GyroscopeEvent(0, 0, 0, DateTime.now()); + + final List _xData = []; + final List _yData = []; + final List _zData = []; + + final List xData = []; + final List yData = []; + final List zData = []; + + final int _maxLength = 50; + double _xMin = 0, _xMax = 0; + double _yMin = 0, _yMax = 0; + double _zMin = 0, _zMax = 0; + + double get xValue => _gyroscopeEvent.x; + double get yValue => _gyroscopeEvent.y; + double get zValue => _gyroscopeEvent.z; + + double get xMin => _xMin; + double get xMax => _xMax; + double get yMin => _yMin; + double get yMax => _yMax; + double get zMin => _zMin; + double get zMax => _zMax; + + bool get isListening => _gyroscopeSubscription != null; + + void initializeSensors() { + if (_gyroscopeSubscription != null) return; + + _gyroscopeSubscription = gyroscopeEventStream().listen( + (event) { + _gyroscopeEvent = event; + _updateData(); + notifyListeners(); + }, + onError: (error) { + logger.e("Gyroscope error: $error"); + }, + cancelOnError: true, + ); + } + + void disposeSensors() { + _gyroscopeSubscription?.cancel(); + _gyroscopeSubscription = null; + } + + void _updateData() { + final x = _gyroscopeEvent.x; + final y = _gyroscopeEvent.y; + final z = _gyroscopeEvent.z; + + _xData.add(x); + _yData.add(y); + _zData.add(z); + + if (_xData.length > _maxLength) _xData.removeAt(0); + if (_yData.length > _maxLength) _yData.removeAt(0); + if (_zData.length > _maxLength) _zData.removeAt(0); + + _xMin = _xData.reduce(min); + _xMax = _xData.reduce(max); + _yMin = _yData.reduce(min); + _yMax = _yData.reduce(max); + _zMin = _zData.reduce(min); + _zMax = _zData.reduce(max); + + xData.clear(); + yData.clear(); + zData.clear(); + + for (int i = 0; i < _xData.length; i++) { + xData.add(FlSpot(i.toDouble(), _xData[i])); + yData.add(FlSpot(i.toDouble(), _yData[i])); + zData.add(FlSpot(i.toDouble(), _zData[i])); + } + notifyListeners(); + } + + List getAxisData(String axis) { + switch (axis) { + case 'x': + return xData; + case 'y': + return yData; + case 'z': + return zData; + default: + return []; + } + } + + double getMin(String axis) { + switch (axis) { + case 'x': + return _xMin; + case 'y': + return _yMin; + case 'z': + return _zMin; + default: + return 0.0; + } + } + + double getMax(String axis) { + switch (axis) { + case 'x': + return _xMax; + case 'y': + return _yMax; + case 'z': + return _zMax; + default: + return 0.0; + } + } + + double getCurrent(String axis) { + switch (axis) { + case 'x': + return _gyroscopeEvent.x; + case 'y': + return _gyroscopeEvent.y; + case 'z': + return _gyroscopeEvent.z; + default: + return 0.0; + } + } + + int getDataLength(String axis) { + switch (axis) { + case 'x': + return xData.length; + case 'y': + return yData.length; + case 'z': + return zData.length; + default: + return 0; + } + } + + @override + void dispose() { + disposeSensors(); + super.dispose(); + } +} diff --git a/lib/view/gyroscope_screen.dart b/lib/view/gyroscope_screen.dart new file mode 100644 index 000000000..2bc943643 --- /dev/null +++ b/lib/view/gyroscope_screen.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/providers/gyroscope_state_provider.dart'; +import 'package:pslab/constants.dart'; +import 'package:pslab/view/widgets/gyroscope_card.dart'; +import 'package:pslab/view/widgets/common_scaffold_widget.dart'; + +class GyroscopeScreen extends StatefulWidget { + const GyroscopeScreen({super.key}); + + @override + State createState() => _GyroscopeScreenState(); +} + +class _GyroscopeScreenState extends State { + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => GyroscopeProvider()..initializeSensors(), + ), + ], + child: CommonScaffold( + title: gyroscopeTitle, + body: SafeArea( + child: Column( + children: [ + Expanded( + child: GyroscopeCard(color: Colors.yellow, axis: xAxis), + ), + Expanded( + child: GyroscopeCard(color: Colors.purple, axis: yAxis), + ), + Expanded( + child: GyroscopeCard(color: Colors.green, axis: zAxis), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/view/instruments_screen.dart b/lib/view/instruments_screen.dart index 4ead61226..4ca570920 100644 --- a/lib/view/instruments_screen.dart +++ b/lib/view/instruments_screen.dart @@ -38,6 +38,18 @@ class _InstrumentsScreenState extends State { ); } break; + case 10: + if (Navigator.canPop(context) && + ModalRoute.of(context)?.settings.name == '/gyroscope') { + Navigator.popUntil(context, ModalRoute.withName('/gyroscope')); + } else { + Navigator.pushNamedAndRemoveUntil( + context, + '/gyroscope', + (route) => route.isFirst, + ); + } + break; default: break; } diff --git a/lib/view/widgets/gyroscope_card.dart b/lib/view/widgets/gyroscope_card.dart new file mode 100644 index 000000000..e38f89da7 --- /dev/null +++ b/lib/view/widgets/gyroscope_card.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:pslab/providers/gyroscope_state_provider.dart'; +import 'package:pslab/constants.dart'; + +class GyroscopeCard extends StatefulWidget { + final String axis; + final Color color; + + const GyroscopeCard({required this.axis, required this.color, super.key}); + + @override + State createState() => _GyroscopeCardState(); +} + +class _GyroscopeCardState extends State { + Widget sideTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle( + color: Colors.white, + fontSize: 9, + ); + return SideTitleWidget( + meta: meta, + child: Text( + maxLines: 1, + meta.formattedValue, + style: style, + ), + ); + } + + @override + Widget build(BuildContext context) { + GyroscopeProvider provider = Provider.of(context); + + List spots = provider.getAxisData(widget.axis.toLowerCase()); + double currVal = provider.getCurrent(widget.axis.toLowerCase()); + double minVal = provider.getMin(widget.axis.toLowerCase()); + double maxVal = provider.getMax(widget.axis.toLowerCase()); + int dataLength = provider.getDataLength(widget.axis.toLowerCase()); + + double chartMinY = -20; + double chartMaxY = 20; + + String axisImage = + 'assets/images/phone_${widget.axis.toLowerCase()}_axis.png'; + + return Card( + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + elevation: 2, + child: Container( + decoration: BoxDecoration( + color: Colors.white, borderRadius: BorderRadius.circular(5)), + child: Row( + children: [ + Expanded( + flex: 30, + child: Column(children: [ + Container( + margin: const EdgeInsets.all(15), + child: Image.asset( + axisImage, + width: 50, + height: 50, + errorBuilder: (context, error, stackTrace) { + IconData fallbackIcon; + switch (widget.axis.toLowerCase()) { + case 'x': + fallbackIcon = Icons.rotate_left; + break; + case 'y': + fallbackIcon = Icons.rotate_right; + break; + case 'z': + fallbackIcon = Icons.rotate_90_degrees_ccw; + break; + default: + fallbackIcon = Icons.rotate_left; + } + return Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + fallbackIcon, + color: widget.color, + size: 30, + ), + ); + }, + ), + ), + Container( + margin: const EdgeInsets.only(top: 8, bottom: 12), + child: Text( + "${currVal.toStringAsFixed(1)} $gyroscopeAxisLabel", + style: const TextStyle(fontSize: 14), + ), + ), + Container( + alignment: Alignment.topLeft, + margin: const EdgeInsets.only(left: 8, top: 4), + child: Text( + "$minValue ${minVal.toStringAsFixed(1)} $gyroscopeAxisLabel", + style: const TextStyle(fontSize: 10), + ), + ), + Container( + alignment: Alignment.topLeft, + margin: const EdgeInsets.only(left: 8, top: 2), + child: Text( + "$maxValue ${maxVal.toStringAsFixed(1)} $gyroscopeAxisLabel", + style: const TextStyle(fontSize: 10), + ), + ), + ]), + ), + Expanded( + flex: 70, + child: Container( + padding: const EdgeInsets.only(bottom: 20, top: 10, right: 25), + color: Colors.black, + child: LineChart( + LineChartData( + backgroundColor: Colors.black, + titlesData: FlTitlesData( + show: true, + topTitles: AxisTitles( + axisNameWidget: Padding( + padding: const EdgeInsets.only(left: 25), + child: Text( + timeAxisLabel, + style: const TextStyle( + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + axisNameSize: 20, + ), + bottomTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false)), + leftTitles: AxisTitles( + axisNameWidget: Text( + gyroscopeAxisLabel, + style: const TextStyle( + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + sideTitles: SideTitles( + reservedSize: 30, + showTitles: true, + getTitlesWidget: sideTitleWidgets, + interval: 10, + ), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false)), + ), + gridData: const FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: true, + horizontalInterval: 10, + ), + borderData: FlBorderData( + show: true, + border: const Border( + bottom: BorderSide( + color: Colors.white38, + ), + left: BorderSide( + color: Colors.white38, + ), + top: BorderSide( + color: Colors.white38, + ), + right: BorderSide( + color: Colors.white38, + ), + ), + ), + minY: chartMinY, + maxY: chartMaxY, + maxX: dataLength > 50 ? 50 : dataLength.toDouble(), + minX: 0, + clipData: const FlClipData.all(), + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: true, + color: widget.color, + barWidth: 2, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } +}