diff --git a/lib/widgets/dialogs/settings_dialog.dart b/lib/widgets/dialogs/settings_dialog.dart index d75d281a..b750f18a 100644 --- a/lib/widgets/dialogs/settings_dialog.dart +++ b/lib/widgets/dialogs/settings_dialog.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -57,7 +58,9 @@ class _SettingsDialogState extends State { late FieldImage _selectedField; late Color _teamColor; late String _pplibClientHost; + late num _optimalCurrentLimit; + late num _maxAccel; @override void initState() { @@ -105,6 +108,7 @@ class _SettingsDialogState extends State { Defaults.ntServerAddress; _optimalCurrentLimit = _calculateOptimalCurrentLimit(); + _maxAccel = _calculateMaxAccel(); } @override @@ -180,6 +184,7 @@ class _SettingsDialogState extends State { _mass = value; _optimalCurrentLimit = _calculateOptimalCurrentLimit(); + _maxAccel = _calculateMaxAccel(); }); } widget.onSettingsChanged(); @@ -265,6 +270,7 @@ class _SettingsDialogState extends State { _wheelRadius = value; _optimalCurrentLimit = _calculateOptimalCurrentLimit(); + _maxAccel = _calculateMaxAccel(); }); } widget.onSettingsChanged(); @@ -285,6 +291,7 @@ class _SettingsDialogState extends State { _driveGearing = value; _optimalCurrentLimit = _calculateOptimalCurrentLimit(); + _maxAccel = _calculateMaxAccel(); }); } widget.onSettingsChanged(); @@ -307,6 +314,9 @@ class _SettingsDialogState extends State { PrefsKeys.maxDriveSpeed, value.toDouble()); setState(() { _maxDriveSpeed = value; + _optimalCurrentLimit = + _calculateOptimalCurrentLimit(); + _maxAccel = _calculateMaxAccel(); }); } widget.onSettingsChanged(); @@ -327,6 +337,7 @@ class _SettingsDialogState extends State { _wheelCOF = value; _optimalCurrentLimit = _calculateOptimalCurrentLimit(); + _maxAccel = _calculateMaxAccel(); }); } widget.onSettingsChanged(); @@ -381,6 +392,8 @@ class _SettingsDialogState extends State { _driveMotor = newValue; _optimalCurrentLimit = _calculateOptimalCurrentLimit(); + _maxAccel = + _calculateMaxAccel(); }); widget.prefs.setString( PrefsKeys.driveMotor, @@ -430,6 +443,54 @@ class _SettingsDialogState extends State { ), ), ), + Tooltip( + richMessage: WidgetSpan( + alignment: + PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + child: Container( + constraints: const BoxConstraints( + maxWidth: 250), + child: Text( + 'The real max acceleration of the robot, calculated from the above values. If this is too slow, ensure that your True Max Drive Speed is correct. This should be the actual measured max speed of the robot under load.', + style: TextStyle( + color: + colorScheme.onSurfaceVariant, + ), + ), + ), + ), + decoration: BoxDecoration( + color: + colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.all( + Radius.circular(4)), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + const Icon( + Icons.help_outline, + size: 16.0, + color: Colors.grey, + ), + const SizedBox(width: 2), + Padding( + padding: const EdgeInsets.only( + bottom: 2), + child: Text( + 'Max Accel: ${_maxAccel.toStringAsFixed(1)}M/S²', + style: TextStyle( + color: (_maxAccel < 3) + ? colorScheme.error + : colorScheme.onSurface, + ), + ), + ), + ], + ), + ), ], ), Padding( @@ -475,18 +536,55 @@ class _SettingsDialogState extends State { value.roundToDouble()); setState(() { _currentLimit = value.roundToDouble(); + _maxAccel = _calculateMaxAccel(); }); } widget.onSettingsChanged(); }, ), - Text( - 'Max Optimal Limit: ${_optimalCurrentLimit.toStringAsFixed(0)}A', - style: TextStyle( - color: (_optimalCurrentLimit.round() < - _currentLimit.round()) - ? colorScheme.error - : colorScheme.onSurface, + const SizedBox(height: 4), + Tooltip( + richMessage: WidgetSpan( + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + child: Container( + constraints: + const BoxConstraints(maxWidth: 250), + child: Text( + 'The maximum current limit that would still prevent the wheels from slipping under maximum acceleration.', + style: TextStyle( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: + const BorderRadius.all(Radius.circular(4)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.help_outline, + size: 16.0, + color: Colors.grey, + ), + const SizedBox(width: 2), + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text( + 'Max Optimal Limit: ${_optimalCurrentLimit.toStringAsFixed(0)}A', + style: TextStyle( + color: (_optimalCurrentLimit.round() < + _currentLimit.round()) + ? colorScheme.error + : colorScheme.onSurface, + ), + ), + ), + ], ), ), ], @@ -657,6 +755,7 @@ class _SettingsDialogState extends State { _holonomicMode = value; _optimalCurrentLimit = _calculateOptimalCurrentLimit(); + _maxAccel = _calculateMaxAccel(); }); widget.onSettingsChanged(); }, @@ -984,8 +1083,36 @@ class _SettingsDialogState extends State { final int numMotors = _holonomicMode ? 1 : 2; final DCMotor driveMotor = DCMotor.fromString(_driveMotor, numMotors).withReduction(_driveGearing); + final maxVelCurrent = min( + driveMotor.getCurrent(_maxDriveSpeed / _wheelRadius, 12.0), + _currentLimit * numMotors); + final torqueLoss = max(driveMotor.getTorque(maxVelCurrent), 0.0); final num moduleFrictionForce = (_wheelCOF * (_mass * 9.8)) / numModules; final num maxFrictionTorque = moduleFrictionForce * _wheelRadius; - return (maxFrictionTorque / driveMotor.kTNMPerAmp) / numMotors; + return ((maxFrictionTorque + torqueLoss) / driveMotor.kTNMPerAmp) / + numMotors; + } + + num _calculateMaxAccel() { + final int numModules = _holonomicMode ? 4 : 2; + final int numMotors = _holonomicMode ? 1 : 2; + final DCMotor driveMotor = + DCMotor.fromString(_driveMotor, numMotors).withReduction(_driveGearing); + + final maxVelCurrent = min( + driveMotor.getCurrent(_maxDriveSpeed / _wheelRadius, 12.0), + _currentLimit * numMotors); + final torqueLoss = max(driveMotor.getTorque(maxVelCurrent), 0.0); + final num moduleFrictionForce = (_wheelCOF * (_mass * 9.8)) / numModules; + final maxCurrent = + min(driveMotor.getCurrent(0.0, 12.0), (_currentLimit * numMotors)); + final maxTorque = ((maxCurrent * driveMotor.kTNMPerAmp) - torqueLoss); + final maxForce = min(maxTorque / _wheelRadius, moduleFrictionForce); + + if (maxForce > 0) { + return (maxForce * numModules) / _mass; + } else { + return 0.0; + } } } diff --git a/test/widgets/dialogs/settings_dialog_test.dart b/test/widgets/dialogs/settings_dialog_test.dart index effb53eb..7131a3a1 100644 --- a/test/widgets/dialogs/settings_dialog_test.dart +++ b/test/widgets/dialogs/settings_dialog_test.dart @@ -6,6 +6,8 @@ import 'package:pathplanner/widgets/field_image.dart'; import 'package:pathplanner/widgets/number_text_field.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../../test_helpers.dart'; + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -44,7 +46,8 @@ void main() { }); testWidgets('bumper width text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -74,7 +77,8 @@ void main() { }); testWidgets('bumper length text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -104,7 +108,8 @@ void main() { }); testWidgets('robot mass text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -134,7 +139,8 @@ void main() { }); testWidgets('robot moi text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -164,7 +170,8 @@ void main() { }); testWidgets('robot wheelbase text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -194,7 +201,8 @@ void main() { }); testWidgets('robot trackwidth text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -224,7 +232,8 @@ void main() { }); testWidgets('wheel radius text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -254,7 +263,8 @@ void main() { }); testWidgets('drive gearing text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -284,7 +294,8 @@ void main() { }); testWidgets('max drive speed field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -315,7 +326,8 @@ void main() { }); testWidgets('wheel cof text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -345,7 +357,8 @@ void main() { }); testWidgets('drive motor dropdown', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -382,7 +395,8 @@ void main() { }); testWidgets('current limit field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -413,7 +427,8 @@ void main() { }); testWidgets('default max vel text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -444,7 +459,8 @@ void main() { }); testWidgets('default max accel text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -475,7 +491,8 @@ void main() { }); testWidgets('default max ang vel text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -506,7 +523,8 @@ void main() { }); testWidgets('default max ang accel text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -537,7 +555,8 @@ void main() { }); testWidgets('field image dropdown', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -574,7 +593,8 @@ void main() { }); testWidgets('team color picker', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -609,7 +629,8 @@ void main() { }); testWidgets('telemetry host text field', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -640,7 +661,8 @@ void main() { }); testWidgets('holonomic mode chip', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold( @@ -672,7 +694,8 @@ void main() { }); testWidgets('hot reload chip', (widgetTester) async { - await widgetTester.binding.setSurfaceSize(const Size(1280, 720)); + FlutterError.onError = ignoreOverflowErrors; + await widgetTester.binding.setSurfaceSize(const Size(1280, 800)); await widgetTester.pumpWidget(MaterialApp( home: Scaffold(