From 9d831a79db5f69c1f678edb415f799eb16a5d14e Mon Sep 17 00:00:00 2001 From: rahul31124 Date: Sat, 7 Jun 2025 16:36:44 +0530 Subject: [PATCH 1/3] Ported robotic arm --- lib/communication/science_lab.dart | 40 +++ lib/constants.dart | 21 ++ lib/main.dart | 2 + lib/view/instruments_screen.dart | 12 + lib/view/robotic_arm_screen.dart | 343 +++++++++++++++++++++ lib/view/widgets/robotic_arm_timeline.dart | 112 +++++++ lib/view/widgets/servo_card.dart | 123 ++++++++ pubspec.yaml | 2 +- 8 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 lib/view/robotic_arm_screen.dart create mode 100644 lib/view/widgets/robotic_arm_timeline.dart create mode 100644 lib/view/widgets/servo_card.dart diff --git a/lib/communication/science_lab.dart b/lib/communication/science_lab.dart index 803e6819d..a43a40c96 100644 --- a/lib/communication/science_lab.dart +++ b/lib/communication/science_lab.dart @@ -397,4 +397,44 @@ class ScienceLab { logger.e(e); } } + + Future servo4( + double angle1, double angle2, double angle3, double angle4) async { + const int period = 10000; + const int base = 750; + const int range = 1900; + const int params = (1 << 5) | 2; + + try { + mPacketHandler.sendByte(mCommandsProto.wavegen); + + mPacketHandler.sendByte(mCommandsProto.sqr4); + + mPacketHandler.sendInt(period); + + int pulse1 = base + (angle1 * range ~/ 180); + mPacketHandler.sendInt(pulse1); + + mPacketHandler.sendInt(0); + + int pulse2 = base + (angle2 * range ~/ 180); + mPacketHandler.sendInt(pulse2); + + mPacketHandler.sendInt(0); + + int pulse3 = base + (angle3 * range ~/ 180); + mPacketHandler.sendInt(pulse3); + + mPacketHandler.sendInt(0); + + int pulse4 = base + (angle4 * range ~/ 180); + mPacketHandler.sendInt(pulse4); + + mPacketHandler.sendByte(params); + + await mPacketHandler.getAcknowledgement(); + } catch (e) { + logger.e("Error in servo4(): $e"); + } + } } diff --git a/lib/constants.dart b/lib/constants.dart index 323a4c469..ed25734be 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -144,3 +144,24 @@ String maxValue = 'Max: '; String gyroscopeTitle = "Gyroscope"; String gyroscopeAxisLabel = 'rad/s'; String noData = 'No data available'; +String degreeSymbol = '°'; +String enterAngleRange = 'Enter angle (0 - 360)'; +String errorCannotBeEmpty = 'Cannot be empty'; +String servoValidNumberRange = 'Please enter a valid number between 0 and 360'; +String ok = 'Ok'; +String roboticArm = 'Robotic Arm'; +String play = 'Play'; +String pause = 'Pause'; +String stop = 'Stop'; +String controls = 'Controls'; +String saveData = 'Save Data'; +String showGuide = 'Show Guide'; +String showLoggedDataKey = 'show_logged_data'; +String showLoggedData = 'Show Logged Data'; +String setAngle = 'Set angle for Servo'; +List servoLabels = [ + 'Servo 1', + 'Servo 2', + 'Servo 3', + 'Servo 4', +]; diff --git a/lib/main.dart b/lib/main.dart index 47decd45a..55303c177 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ 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/robotic_arm_screen.dart'; import 'package:pslab/view/settings_screen.dart'; import 'package:pslab/view/about_us_screen.dart'; import 'package:pslab/view/software_licenses_screen.dart'; @@ -53,6 +54,7 @@ class MyApp extends StatelessWidget { '/softwareLicenses': (context) => const SoftwareLicensesScreen(), '/accelerometer': (context) => const AccelerometerScreen(), '/gyroscope': (context) => const GyroscopeScreen(), + '/roboticArm': (context) => const RoboticArmScreen(), }, ); } diff --git a/lib/view/instruments_screen.dart b/lib/view/instruments_screen.dart index 4ca570920..a490c675d 100644 --- a/lib/view/instruments_screen.dart +++ b/lib/view/instruments_screen.dart @@ -50,6 +50,18 @@ class _InstrumentsScreenState extends State { ); } break; + case 12: + if (Navigator.canPop(context) && + ModalRoute.of(context)?.settings.name == '/roboticArm') { + Navigator.popUntil(context, ModalRoute.withName('/roboticArm')); + } else { + Navigator.pushNamedAndRemoveUntil( + context, + '/roboticArm', + (route) => route.isFirst, + ); + } + break; default: break; } diff --git a/lib/view/robotic_arm_screen.dart b/lib/view/robotic_arm_screen.dart new file mode 100644 index 000000000..a9697da62 --- /dev/null +++ b/lib/view/robotic_arm_screen.dart @@ -0,0 +1,343 @@ +import 'dart:async'; +import 'dart:developer' as developer; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pslab/colors.dart'; +import 'package:pslab/communication/science_lab.dart'; +import 'package:pslab/constants.dart'; +import 'package:pslab/view/widgets/robotic_arm_timeline.dart'; +import '../others/science_lab_common.dart'; +import 'widgets/servo_card.dart'; + +class RoboticArmScreen extends StatefulWidget { + const RoboticArmScreen({super.key}); + + @override + State createState() => _RoboticArmScreenState(); +} + +class _RoboticArmScreenState extends State { + List servoValues = [0, 0, 0, 0]; + ScrollController timelineScrollController = ScrollController(); + Timer? _debounceTimer; + Timer? timelineTimer; + int timelinePosition = 0; + bool isPlaying = false; + late ScienceLab scienceLab; + final int totalTimelineItems = 60; + final double scrollAmountPerTick = 120; + List> timelineDegrees = + List.generate(60, (_) => List.filled(4, 0)); + int selectedServoIndex = 0; + double selectedServoValue = 0.0; + @override + void initState() { + super.initState(); + _setLandscapeOrientation(); + scienceLab = + ScienceLabCommon(ScienceLabCommon.communicationHandler).getScienceLab(); + Future.microtask(() async { + await scienceLab.connect(); + }); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + } + + void _sendAllServoCommands(List servoValues) { + _debounceTimer?.cancel(); + + _debounceTimer = Timer(const Duration(milliseconds: 200), () async { + try { + await scienceLab.servo4( + servoValues[0], + servoValues[1], + servoValues[2], + servoValues[3], + ); + } catch (e) { + developer.log("Servo command failed", error: e, name: 'ServoControl'); + } + }); + } + + void _setLandscapeOrientation() { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + } + + @override + void dispose() { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + super.dispose(); + } + + void togglePlayPause() { + if (isPlaying) { + timelineTimer?.cancel(); + setState(() { + isPlaying = false; + }); + } else { + timelineTimer?.cancel(); + + timelineTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { + if (timelinePosition >= totalTimelineItems) { + stopScrolling(resetPosition: false); + return; + } + + timelineScrollController.animateTo( + timelineScrollController.offset + scrollAmountPerTick, + duration: const Duration(milliseconds: 50), + curve: Curves.easeInOut, + ); + + List currentAngles = timelineDegrees[timelinePosition]; + + if (scienceLab.isConnected()) { + try { + await scienceLab.servo4( + currentAngles[0], + currentAngles[1], + currentAngles[2], + currentAngles[3], + ); + } catch (e) { + developer.log("Servo timeline failed", + error: e, name: 'ServoControl'); + } + } + + setState(() { + timelinePosition++; + }); + }); + + setState(() { + isPlaying = true; + }); + } + } + + void stopScrolling({bool resetPosition = true}) { + timelineTimer?.cancel(); + setState(() { + isPlaying = false; + timelinePosition = 0; + }); + if (resetPosition) { + timelineScrollController.animateTo( + 0, + duration: const Duration(milliseconds: 50), + curve: Curves.easeOut, + ); + } + } + + void _showAngleInputDialog(int index) { + double currentValue = servoValues[index]; + final controller = + TextEditingController(text: currentValue.toStringAsFixed(0)); + + showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, setStateDialog) { + void updateValue(double newValue) { + if (newValue < 0) newValue = 0; + if (newValue > 360) newValue = 360; + currentValue = newValue; + controller.text = currentValue.toStringAsFixed(0); + controller.selection = TextSelection.fromPosition( + TextPosition(offset: controller.text.length), + ); + } + + return AlertDialog( + title: Text('$setAngle${index + 1}'), + content: Row( + children: [ + IconButton( + icon: const Icon(Icons.remove_circle_outline), + onPressed: () { + setStateDialog(() { + updateValue(currentValue - 1); + }); + }, + ), + Expanded( + child: TextField( + controller: controller, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + decoration: InputDecoration( + hintText: enterAngleRange, + ), + onChanged: (val) { + final parsed = double.tryParse(val); + if (parsed != null && parsed >= 0 && parsed <= 360) { + setStateDialog(() { + currentValue = parsed; + }); + } + }, + ), + ), + IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: () { + setStateDialog(() { + updateValue(currentValue + 1); + }); + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + final text = controller.text.trim(); + final value = double.tryParse(text); + + if (text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(errorCannotBeEmpty)), + ); + return; + } + + if (value == null || value < 0 || value > 360) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(servoValidNumberRange)), + ); + return; + } + + setState(() { + servoValues[index] = value; + }); + Navigator.of(context).pop(); + }, + child: Text(ok), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(cancel), + ), + ], + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + final servoWidth = (screenWidth / 4) - 7; + final servoHeight = (screenHeight / 2.9); + + return Scaffold( + appBar: AppBar( + title: Text( + roboticArm, + style: const TextStyle(color: Colors.white), + ), + backgroundColor: primaryRed, + iconTheme: const IconThemeData(color: Colors.white), + actions: [ + IconButton( + icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow), + tooltip: play, + onPressed: togglePlayPause, + ), + IconButton( + icon: const Icon(Icons.stop), + tooltip: stop, + onPressed: () { + stopScrolling(resetPosition: true); + }, + ), + IconButton( + icon: const Icon(Icons.tune), // TODO: Controls + tooltip: controls, + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.save), + tooltip: saveData, + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.info), + tooltip: showGuide, + onPressed: () {}, // TODO: + ), + PopupMenuButton( + onSelected: (value) { + if (value == showLoggedDataKey) {} // TODO: + }, + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: showLoggedDataKey, + child: Text(showLoggedData), + ), + ], + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: servoHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(4, (index) { + return SizedBox( + width: servoWidth, + height: servoHeight, + child: ServoCard( + value: servoValues[index], + label: servoLabels[index], + servoId: index, + onChanged: (val) { + setState(() => servoValues[index] = val); + _sendAllServoCommands(servoValues); + }, + onTap: () => _showAngleInputDialog(index), + ), + ); + }), + ), + ), + const SizedBox(height: 5), + SizedBox( + width: screenWidth * 5, + child: TimelineScrollView( + scrollController: timelineScrollController, + timelinePosition: timelinePosition, + scrollAmountPerTick: scrollAmountPerTick, + timelineDegrees: timelineDegrees, + onUpdate: (index, servo, value) { + setState(() { + timelineDegrees[index][servo] = value; + }); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/widgets/robotic_arm_timeline.dart b/lib/view/widgets/robotic_arm_timeline.dart new file mode 100644 index 000000000..df3ee1e39 --- /dev/null +++ b/lib/view/widgets/robotic_arm_timeline.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:pslab/constants.dart'; + +import '../../colors.dart'; + +class TimelineScrollView extends StatelessWidget { + final ScrollController scrollController; + final int timelinePosition; + final double scrollAmountPerTick; + final List> timelineDegrees; + final void Function(int index, int servo, double value) onUpdate; + + const TimelineScrollView({ + super.key, + required this.scrollController, + required this.timelinePosition, + required this.scrollAmountPerTick, + required this.timelineDegrees, + required this.onUpdate, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 160, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: scrollController, + child: Row( + children: List.generate(60, (index) { + bool isCurrent = index == timelinePosition; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 0), + child: Column( + children: [ + Container( + width: 130, + height: 4, + color: isCurrent ? primaryRed : Colors.transparent, + ), + const SizedBox(height: 3), + ...List.generate(4, (boxIndex) { + return Padding( + padding: const EdgeInsets.all(1.0), + child: SizedBox( + width: 130, + height: 35, + child: DragTarget>( + builder: (context, candidateData, rejectedData) { + bool isHighlighted = candidateData.isNotEmpty; + return Container( + decoration: BoxDecoration( + color: isHighlighted + ? Colors.blue.withAlpha((0.3 * 255).round()) + : Colors.black, + borderRadius: BorderRadius.circular(4), + ), + padding: + const EdgeInsets.symmetric(horizontal: 2), + child: Stack( + children: [ + Positioned( + top: 0, + left: 1, + child: Text( + '${timelineDegrees[index][boxIndex].toStringAsFixed(0)}$degreeSymbol', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ), + Positioned( + bottom: 0, + right: 1, + child: Text( + '${index + 1}s', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + ), + ], + ), + ); + }, + onWillAcceptWithDetails: + (DragTargetDetails> + details) { + final data = details.data; + return data['servoId'] == boxIndex; + }, + onAcceptWithDetails: + (DragTargetDetails> + details) { + final data = details.data; + onUpdate(index, boxIndex, data['degree']); + }, + ), + ), + ); + }), + ], + ), + ); + }), + ), + ), + ); + } +} diff --git a/lib/view/widgets/servo_card.dart b/lib/view/widgets/servo_card.dart new file mode 100644 index 000000000..35c47b584 --- /dev/null +++ b/lib/view/widgets/servo_card.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:pslab/colors.dart'; +import 'package:sleek_circular_slider/sleek_circular_slider.dart'; + +import '../../constants.dart'; + +class ServoCard extends StatelessWidget { + final double value; + final Function(double) onChanged; + final VoidCallback onTap; + final String label; + final int servoId; + + const ServoCard({ + super.key, + required this.value, + required this.onChanged, + required this.onTap, + required this.label, + required this.servoId, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 120, + height: 180, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade400), + ), + ), + ), + Positioned( + top: 6, + left: 8, + child: Text( + label, + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + ), + ), + Positioned( + top: 8, + right: 8, + child: Draggable>( + data: { + 'servoId': servoId, + 'degree': value, + }, + feedback: Material( + color: Colors.transparent, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${value.toInt()}°', + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + childWhenDragging: + const Icon(Icons.more_vert, size: 16, color: Colors.grey), + child: const Icon(Icons.more_vert, size: 16, color: Colors.grey), + ), + ), + Positioned( + left: 0, + right: 0, + top: 25, + child: GestureDetector( + onTap: onTap, + child: Center( + child: SleekCircularSlider( + initialValue: value, + min: 0, + max: 360, + onChange: onChanged, + appearance: CircularSliderAppearance( + size: 96, + startAngle: 270, + angleRange: 360, + customWidths: CustomSliderWidths( + trackWidth: 8, + progressBarWidth: 8, + handlerSize: 14, + ), + customColors: CustomSliderColors( + trackColor: Colors.grey.shade300, + progressBarColor: primaryRed, + dotColor: Colors.blue, + ), + infoProperties: InfoProperties( + mainLabelStyle: const TextStyle(fontSize: 16), + modifier: (val) => '${val.toInt()}$degreeSymbol', + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d3490730a..da46a2553 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,7 +55,7 @@ dependencies: font_awesome_flutter: ^10.8.0 package_info_plus: ^8.3.0 sensors_plus: ^6.1.1 - + sleek_circular_slider: ^2.1.0 dev_dependencies: From ae285fad13c999facb59c07576f60dd85bdf2f72 Mon Sep 17 00:00:00 2001 From: rahul31124 Date: Sun, 8 Jun 2025 15:16:42 +0530 Subject: [PATCH 2/3] SOC Principle --- lib/constants.dart | 1 + lib/providers/robotic_arm_state_provider.dart | 127 +++++ lib/view/robotic_arm_screen.dart | 537 ++++++++---------- 3 files changed, 364 insertions(+), 301 deletions(-) create mode 100644 lib/providers/robotic_arm_state_provider.dart diff --git a/lib/constants.dart b/lib/constants.dart index ed25734be..a62a1ea86 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -159,6 +159,7 @@ String showGuide = 'Show Guide'; String showLoggedDataKey = 'show_logged_data'; String showLoggedData = 'Show Logged Data'; String setAngle = 'Set angle for Servo'; +String angleDialog = 'AngleDialog'; List servoLabels = [ 'Servo 1', 'Servo 2', diff --git a/lib/providers/robotic_arm_state_provider.dart b/lib/providers/robotic_arm_state_provider.dart new file mode 100644 index 000000000..4e9d92886 --- /dev/null +++ b/lib/providers/robotic_arm_state_provider.dart @@ -0,0 +1,127 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pslab/communication/science_lab.dart'; +import 'package:pslab/others/logger_service.dart'; +import '../others/science_lab_common.dart'; + +class RoboticArmStateProvider extends ChangeNotifier { + final List servoValues = [0, 0, 0, 0]; + final int totalTimelineItems = 60; + final double scrollAmountPerTick = 120; + final List> timelineDegrees = + List.generate(60, (_) => List.filled(4, 0)); + int timelinePosition = 0; + bool isPlaying = false; + + late ScienceLab scienceLab; + Timer? _debounceTimer; + Timer? _timelineTimer; + final ScrollController timelineScrollController = ScrollController(); + + Future initialize() async { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + scienceLab = + ScienceLabCommon(ScienceLabCommon.communicationHandler).getScienceLab(); + await scienceLab.connect(); + } + + void disposeResources() { + _timelineTimer?.cancel(); + _debounceTimer?.cancel(); + timelineScrollController.dispose(); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + } + + void updateServoValue(int index, double value) { + servoValues[index] = value; + notifyListeners(); + _sendAllServoCommands(); + } + + void updateTimelineDegree(int timeIndex, int servoIndex, double value) { + timelineDegrees[timeIndex][servoIndex] = value; + notifyListeners(); + } + + void _sendAllServoCommands() { + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(milliseconds: 200), () async { + try { + await scienceLab.servo4( + servoValues[0], + servoValues[1], + servoValues[2], + servoValues[3], + ); + } catch (e) { + logger.e(e); + } + }); + } + + void togglePlayPause() { + if (isPlaying) { + _timelineTimer?.cancel(); + isPlaying = false; + notifyListeners(); + } else { + _timelineTimer = + Timer.periodic(const Duration(seconds: 1), (timer) async { + if (timelinePosition >= totalTimelineItems) { + stopScrolling(resetPosition: false); + return; + } + + timelineScrollController.animateTo( + timelineScrollController.offset + scrollAmountPerTick, + duration: const Duration(milliseconds: 50), + curve: Curves.easeInOut, + ); + + final angles = timelineDegrees[timelinePosition]; + + if (scienceLab.isConnected()) { + try { + await scienceLab.servo4( + angles[0], + angles[1], + angles[2], + angles[3], + ); + } catch (e) { + logger.e(e); + } + } + + timelinePosition++; + notifyListeners(); + }); + + isPlaying = true; + notifyListeners(); + } + } + + void stopScrolling({bool resetPosition = true}) { + _timelineTimer?.cancel(); + isPlaying = false; + timelinePosition = 0; + notifyListeners(); + + if (resetPosition) { + timelineScrollController.animateTo( + 0, + duration: const Duration(milliseconds: 50), + curve: Curves.easeOut, + ); + } + } +} diff --git a/lib/view/robotic_arm_screen.dart b/lib/view/robotic_arm_screen.dart index a9697da62..96e6705c7 100644 --- a/lib/view/robotic_arm_screen.dart +++ b/lib/view/robotic_arm_screen.dart @@ -1,12 +1,10 @@ -import 'dart:async'; -import 'dart:developer' as developer; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:pslab/colors.dart'; -import 'package:pslab/communication/science_lab.dart'; +import 'package:provider/provider.dart'; import 'package:pslab/constants.dart'; +import 'package:pslab/view/widgets/common_scaffold_widget.dart'; import 'package:pslab/view/widgets/robotic_arm_timeline.dart'; -import '../others/science_lab_common.dart'; +import '../colors.dart'; +import '../providers/robotic_arm_state_provider.dart'; import 'widgets/servo_card.dart'; class RoboticArmScreen extends StatefulWidget { @@ -17,221 +15,132 @@ class RoboticArmScreen extends StatefulWidget { } class _RoboticArmScreenState extends State { - List servoValues = [0, 0, 0, 0]; - ScrollController timelineScrollController = ScrollController(); - Timer? _debounceTimer; - Timer? timelineTimer; - int timelinePosition = 0; - bool isPlaying = false; - late ScienceLab scienceLab; - final int totalTimelineItems = 60; - final double scrollAmountPerTick = 120; - List> timelineDegrees = - List.generate(60, (_) => List.filled(4, 0)); - int selectedServoIndex = 0; - double selectedServoValue = 0.0; + late RoboticArmStateProvider provider; + @override void initState() { super.initState(); - _setLandscapeOrientation(); - scienceLab = - ScienceLabCommon(ScienceLabCommon.communicationHandler).getScienceLab(); - Future.microtask(() async { - await scienceLab.connect(); - }); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - } - - void _sendAllServoCommands(List servoValues) { - _debounceTimer?.cancel(); - - _debounceTimer = Timer(const Duration(milliseconds: 200), () async { - try { - await scienceLab.servo4( - servoValues[0], - servoValues[1], - servoValues[2], - servoValues[3], - ); - } catch (e) { - developer.log("Servo command failed", error: e, name: 'ServoControl'); - } - }); - } - - void _setLandscapeOrientation() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); - } - - @override - void dispose() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - super.dispose(); + provider = RoboticArmStateProvider(); + provider.initialize(); } - void togglePlayPause() { - if (isPlaying) { - timelineTimer?.cancel(); - setState(() { - isPlaying = false; - }); - } else { - timelineTimer?.cancel(); - - timelineTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { - if (timelinePosition >= totalTimelineItems) { - stopScrolling(resetPosition: false); - return; - } - - timelineScrollController.animateTo( - timelineScrollController.offset + scrollAmountPerTick, - duration: const Duration(milliseconds: 50), - curve: Curves.easeInOut, - ); - - List currentAngles = timelineDegrees[timelinePosition]; - - if (scienceLab.isConnected()) { - try { - await scienceLab.servo4( - currentAngles[0], - currentAngles[1], - currentAngles[2], - currentAngles[3], - ); - } catch (e) { - developer.log("Servo timeline failed", - error: e, name: 'ServoControl'); - } - } - - setState(() { - timelinePosition++; - }); - }); - - setState(() { - isPlaying = true; - }); - } - } - - void stopScrolling({bool resetPosition = true}) { - timelineTimer?.cancel(); - setState(() { - isPlaying = false; - timelinePosition = 0; - }); - if (resetPosition) { - timelineScrollController.animateTo( - 0, - duration: const Duration(milliseconds: 50), - curve: Curves.easeOut, - ); - } - } - - void _showAngleInputDialog(int index) { - double currentValue = servoValues[index]; + void _showAngleInputDialog(BuildContext context, int index) { + double currentValue = provider.servoValues[index]; final controller = - TextEditingController(text: currentValue.toStringAsFixed(0)); + TextEditingController(text: currentValue.round().toString()); - showDialog( + showGeneralDialog( context: context, - builder: (context) { - return StatefulBuilder( - builder: (context, setStateDialog) { - void updateValue(double newValue) { - if (newValue < 0) newValue = 0; - if (newValue > 360) newValue = 360; - currentValue = newValue; - controller.text = currentValue.toStringAsFixed(0); - controller.selection = TextSelection.fromPosition( - TextPosition(offset: controller.text.length), - ); - } + barrierDismissible: true, + barrierLabel: angleDialog, + transitionDuration: const Duration(milliseconds: 200), + pageBuilder: (context, _, __) { + return SafeArea( + child: Align( + alignment: Alignment.topCenter, + child: Material( + color: Colors.transparent, + child: StatefulBuilder( + builder: (context, setStateDialog) { + void updateValue(double newVal) { + currentValue = newVal.clamp(0, 360); + controller.text = currentValue.round().toString(); + } - return AlertDialog( - title: Text('$setAngle${index + 1}'), - content: Row( - children: [ - IconButton( - icon: const Icon(Icons.remove_circle_outline), - onPressed: () { - setStateDialog(() { - updateValue(currentValue - 1); - }); - }, - ), - Expanded( - child: TextField( - controller: controller, - keyboardType: TextInputType.number, - textAlign: TextAlign.center, - decoration: InputDecoration( - hintText: enterAngleRange, - ), - onChanged: (val) { - final parsed = double.tryParse(val); - if (parsed != null && parsed >= 0 && parsed <= 360) { - setStateDialog(() { - currentValue = parsed; - }); - } - }, + return Container( + width: 240, + padding: const EdgeInsets.all(10), + margin: const EdgeInsets.only(top: 20), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: primaryRed), + borderRadius: BorderRadius.circular(10), ), - ), - IconButton( - icon: const Icon(Icons.add_circle_outline), - onPressed: () { - setStateDialog(() { - updateValue(currentValue + 1); - }); - }, - ), - ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('$setAngle ${index + 1}', + style: TextStyle(color: primaryRed)), + const SizedBox(height: 8), + Row( + children: [ + IconButton( + icon: Icon(Icons.remove, + size: 18, color: primaryRed), + onPressed: () { + updateValue( + (double.tryParse(controller.text) ?? 0) - + 1); + setStateDialog(() {}); + }, + ), + Expanded( + child: TextField( + controller: controller, + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + isDense: true, + border: OutlineInputBorder()), + onChanged: (val) { + final parsed = double.tryParse(val); + if (parsed != null) { + currentValue = parsed.clamp(0, 360); + setStateDialog(() {}); + } + }, + ), + ), + IconButton( + icon: + Icon(Icons.add, size: 18, color: primaryRed), + onPressed: () { + updateValue( + (double.tryParse(controller.text) ?? 0) + + 1); + setStateDialog(() {}); + }, + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(cancel)), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: primaryRed, + minimumSize: const Size(50, 32), + ), + onPressed: () { + final value = double.tryParse(controller.text); + if (value != null && + value >= 0 && + value <= 360) { + setState(() { + provider.updateServoValue(index, value); + }); + Navigator.pop(context); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(servoValidNumberRange)), + ); + } + }, + child: Text(ok), + ), + ], + ) + ], + ), + ); + }, ), - actions: [ - TextButton( - onPressed: () { - final text = controller.text.trim(); - final value = double.tryParse(text); - - if (text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(errorCannotBeEmpty)), - ); - return; - } - - if (value == null || value < 0 || value > 360) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(servoValidNumberRange)), - ); - return; - } - - setState(() { - servoValues[index] = value; - }); - Navigator.of(context).pop(); - }, - child: Text(ok), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(cancel), - ), - ], - ); - }, + ), + ), ); }, ); @@ -239,104 +148,130 @@ class _RoboticArmScreenState extends State { @override Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - final screenHeight = MediaQuery.of(context).size.height; - final servoWidth = (screenWidth / 4) - 7; - final servoHeight = (screenHeight / 2.9); - - return Scaffold( - appBar: AppBar( - title: Text( - roboticArm, - style: const TextStyle(color: Colors.white), + return MultiProvider( + providers: [ + ChangeNotifierProvider.value( + value: provider, ), - backgroundColor: primaryRed, - iconTheme: const IconThemeData(color: Colors.white), - actions: [ - IconButton( - icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow), - tooltip: play, - onPressed: togglePlayPause, - ), - IconButton( - icon: const Icon(Icons.stop), - tooltip: stop, - onPressed: () { - stopScrolling(resetPosition: true); - }, - ), - IconButton( - icon: const Icon(Icons.tune), // TODO: Controls - tooltip: controls, - onPressed: () {}, - ), - IconButton( - icon: const Icon(Icons.save), - tooltip: saveData, - onPressed: () {}, - ), - IconButton( - icon: const Icon(Icons.info), - tooltip: showGuide, - onPressed: () {}, // TODO: - ), - PopupMenuButton( - onSelected: (value) { - if (value == showLoggedDataKey) {} // TODO: - }, - itemBuilder: (BuildContext context) => [ - PopupMenuItem( - value: showLoggedDataKey, - child: Text(showLoggedData), - ), - ], - ), - ], - ), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: servoHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate(4, (index) { - return SizedBox( - width: servoWidth, - height: servoHeight, - child: ServoCard( - value: servoValues[index], - label: servoLabels[index], - servoId: index, - onChanged: (val) { - setState(() => servoValues[index] = val); - _sendAllServoCommands(servoValues); - }, - onTap: () => _showAngleInputDialog(index), - ), - ); - }), + ], + child: Consumer( + builder: (context, provider, _) { + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + final servoWidth = (screenWidth / 4) - 7; + final servoHeight = (screenHeight / 2.9); + + return CommonScaffold( + title: roboticArm, + actions: [ + IconButton( + icon: Icon( + provider.isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.white, + ), + tooltip: play, + onPressed: () { + setState(() { + provider.togglePlayPause(); + }); + }, ), - ), - const SizedBox(height: 5), - SizedBox( - width: screenWidth * 5, - child: TimelineScrollView( - scrollController: timelineScrollController, - timelinePosition: timelinePosition, - scrollAmountPerTick: scrollAmountPerTick, - timelineDegrees: timelineDegrees, - onUpdate: (index, servo, value) { + IconButton( + icon: const Icon(Icons.stop, color: Colors.white), + tooltip: stop, + onPressed: () { setState(() { - timelineDegrees[index][servo] = value; + provider.stopScrolling(resetPosition: true); }); }, ), + IconButton( + icon: const Icon(Icons.tune, color: Colors.white), + tooltip: controls, + onPressed: () {}, //TODO controls + ), + IconButton( + icon: const Icon(Icons.save, color: Colors.white), + tooltip: saveData, + onPressed: () {}, //TODO + ), + IconButton( + icon: const Icon(Icons.info, color: Colors.white), + tooltip: showGuide, + onPressed: () {}, //TODO + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert, color: Colors.white), + onSelected: (value) { + if (value == showLoggedDataKey) { + // TODO + } + }, + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: showLoggedDataKey, + child: Text(showLoggedData), + ), + ], + ), + ], + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: servoHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(4, (index) { + return SizedBox( + width: servoWidth, + height: servoHeight, + child: ServoCard( + value: provider.servoValues[index], + label: servoLabels[index], + servoId: index, + onChanged: (val) { + setState(() { + provider.updateServoValue(index, val); + }); + }, + onTap: () => + _showAngleInputDialog(context, index), + ), + ); + }), + ), + ), + const SizedBox(height: 5), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: screenWidth * 5, + child: TimelineScrollView( + scrollController: provider.timelineScrollController, + timelinePosition: provider.timelinePosition, + scrollAmountPerTick: provider.scrollAmountPerTick, + timelineDegrees: provider.timelineDegrees, + onUpdate: (index, servo, value) { + setState(() { + provider.updateTimelineDegree( + index, servo, value); + }); + }, + ), + ), + ), + ), + ], + ), + ), ), - ], - ), + ); + }, ), ); } From e4b69276db0aaccf995c349764cf1e9c58390e91 Mon Sep 17 00:00:00 2001 From: rahul31124 Date: Wed, 11 Jun 2025 11:58:06 +0530 Subject: [PATCH 3/3] Resize --- lib/view/robotic_arm_screen.dart | 44 ++++++++++++++++---------------- lib/view/widgets/servo_card.dart | 2 -- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/lib/view/robotic_arm_screen.dart b/lib/view/robotic_arm_screen.dart index 96e6705c7..72ed3d6c6 100644 --- a/lib/view/robotic_arm_screen.dart +++ b/lib/view/robotic_arm_screen.dart @@ -109,11 +109,7 @@ class _RoboticArmScreenState extends State { TextButton( onPressed: () => Navigator.pop(context), child: Text(cancel)), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: primaryRed, - minimumSize: const Size(50, 32), - ), + TextButton( onPressed: () { final value = double.tryParse(controller.text); if (value != null && @@ -158,8 +154,7 @@ class _RoboticArmScreenState extends State { builder: (context, provider, _) { final screenWidth = MediaQuery.of(context).size.width; final screenHeight = MediaQuery.of(context).size.height; - final servoWidth = (screenWidth / 4) - 7; - final servoHeight = (screenHeight / 2.9); + final servoHeight = (screenHeight / 2.7); return CommonScaffold( title: roboticArm, @@ -226,26 +221,31 @@ class _RoboticArmScreenState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate(4, (index) { - return SizedBox( - width: servoWidth, - height: servoHeight, - child: ServoCard( - value: provider.servoValues[index], - label: servoLabels[index], - servoId: index, - onChanged: (val) { - setState(() { - provider.updateServoValue(index, val); - }); - }, - onTap: () => - _showAngleInputDialog(context, index), + return Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 4), + child: SizedBox( + height: servoHeight, + child: ServoCard( + value: provider.servoValues[index], + label: servoLabels[index], + servoId: index, + onChanged: (val) { + setState(() { + provider.updateServoValue(index, val); + }); + }, + onTap: () => + _showAngleInputDialog(context, index), + ), + ), ), ); }), ), ), - const SizedBox(height: 5), + const SizedBox(height: 3), Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, diff --git a/lib/view/widgets/servo_card.dart b/lib/view/widgets/servo_card.dart index 35c47b584..1b6d3b9fe 100644 --- a/lib/view/widgets/servo_card.dart +++ b/lib/view/widgets/servo_card.dart @@ -23,8 +23,6 @@ class ServoCard extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - width: 120, - height: 180, child: Stack( clipBehavior: Clip.none, children: [