From b0c42e5b302aa557663f01de355c1d6b938710b5 Mon Sep 17 00:00:00 2001 From: Anashuman Singh Date: Fri, 17 Jan 2025 17:05:46 +0530 Subject: [PATCH 1/4] feat: initialized Oscilloscope Screen --- .gitignore | 19 ++ assets/images/channel_parameters.gif | Bin 0 -> 10949 bytes assets/images/data_analysis.png | Bin 0 -> 15336 bytes assets/images/timebase.png | Bin 0 -> 3272 bytes assets/images/xymode.png | Bin 0 -> 21561 bytes lib/main.dart | 2 + .../oscilloscope_state_provider.dart | 12 + lib/view/instruments_screen.dart | 49 ++- lib/view/oscilloscope_screen.dart | 98 ++++++ lib/view/widgets/applications_list_item.dart | 161 +++++---- .../widgets/channel_parameters_widget.dart | 313 ++++++++++++++++++ lib/view/widgets/common_scaffold_widget.dart | 56 ++-- lib/view/widgets/data_analysis_widget.dart | 276 +++++++++++++++ lib/view/widgets/main_scaffold_widget.dart | 83 +++++ lib/view/widgets/navigation_drawer.dart | 23 +- .../widgets/oscilloscope_screen_tabs.dart | 239 +++++++++++++ lib/view/widgets/timebase_trigger_widget.dart | 194 +++++++++++ lib/view/widgets/xyplot_widget.dart | 111 +++++++ pubspec.lock | 44 ++- pubspec.yaml | 7 +- 20 files changed, 1548 insertions(+), 139 deletions(-) create mode 100644 assets/images/channel_parameters.gif create mode 100644 assets/images/data_analysis.png create mode 100644 assets/images/timebase.png create mode 100644 assets/images/xymode.png create mode 100644 lib/providers/oscilloscope_state_provider.dart create mode 100644 lib/view/oscilloscope_screen.dart create mode 100644 lib/view/widgets/channel_parameters_widget.dart create mode 100644 lib/view/widgets/data_analysis_widget.dart create mode 100644 lib/view/widgets/main_scaffold_widget.dart create mode 100644 lib/view/widgets/oscilloscope_screen_tabs.dart create mode 100644 lib/view/widgets/timebase_trigger_widget.dart create mode 100644 lib/view/widgets/xyplot_widget.dart diff --git a/.gitignore b/.gitignore index d1c5f5329..666fc6a56 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,25 @@ migrate_working_dir/ .pub/ /build/ +# macOS +**/Flutter/ephemeral/ +**/Pods/ +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/ephemeral +**/xcuserdata/ + +# Windows +**/windows/flutter/ephemeral/ +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake + +# Linux +**/linux/flutter/ephemeral/ +**/linux/flutter/generated_plugin_registrant.cc +**/linux/flutter/generated_plugin_registrant.h +**/linux/flutter/generated_plugins.cmake + # Symbolication related app.*.symbols diff --git a/assets/images/channel_parameters.gif b/assets/images/channel_parameters.gif new file mode 100644 index 0000000000000000000000000000000000000000..3871f89ff39652e020f127119067e9571ba8f59e GIT binary patch literal 10949 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

&vb3LQh)fh^L97#Kk=_7t>1a! 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 literal 0 HcmV?d00001 diff --git a/assets/images/data_analysis.png b/assets/images/data_analysis.png new file mode 100644 index 0000000000000000000000000000000000000000..fea9d594f3d831f1dcded093e2272ae6eecd88de GIT binary patch literal 15336 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 g5|{Hx*fN5y&7~O(SY1Kkox$N;olX const InstrumentsScreen(), + '/oscilloscope': (context) => const OscilloscopeScreen(), '/connectDevice': (context) => const ConnectDeviceScreen(), '/faq': (context) => const FAQScreen(), }, diff --git a/lib/providers/oscilloscope_state_provider.dart b/lib/providers/oscilloscope_state_provider.dart new file mode 100644 index 000000000..3172ecee7 --- /dev/null +++ b/lib/providers/oscilloscope_state_provider.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class OscilloscopeStateProvider extends ChangeNotifier { + int _selectedIndex = 0; + + int get selectedIndex => _selectedIndex; + + void updateSelectedIndex(int index) { + _selectedIndex = index; + notifyListeners(); + } +} diff --git a/lib/view/instruments_screen.dart b/lib/view/instruments_screen.dart index 42627ee53..bead05324 100644 --- a/lib/view/instruments_screen.dart +++ b/lib/view/instruments_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:pslab/constants.dart'; import 'package:pslab/view/widgets/applications_list_item.dart'; -import 'package:pslab/view/widgets/common_scaffold_widget.dart'; +import 'package:pslab/view/widgets/main_scaffold_widget.dart'; class InstrumentsScreen extends StatefulWidget { const InstrumentsScreen({super.key}); @@ -11,9 +12,42 @@ class InstrumentsScreen extends StatefulWidget { } class _InstrumentsScreenState extends State { + void _onItemTapped(int index) { + switch (index) { + case 0: + Navigator.pushNamed(context, '/oscilloscope'); + break; + default: + break; + } + } + + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + _setOrientation(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + }); + super.initState(); + } + + void _setOrientation() { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + } + + @override + void dispose() { + super.dispose(); + } + @override Widget build(BuildContext context) { - return CommonScaffold( + return MainScaffold( index: 0, title: 'Instruments', body: SafeArea( @@ -22,10 +56,13 @@ class _InstrumentsScreenState extends State { child: ListView.builder( itemCount: instrumentHeadings.length, itemBuilder: (context, index) { - return ApplicationsListItem( - heading: instrumentHeadings[index], - description: instrumentDesc[index], - instrumentIcon: instrumentIcons[index], + return GestureDetector( + onTap: () => _onItemTapped(index), + child: ApplicationsListItem( + heading: instrumentHeadings[index], + description: instrumentDesc[index], + instrumentIcon: instrumentIcons[index], + ), ); }, ), diff --git a/lib/view/oscilloscope_screen.dart b/lib/view/oscilloscope_screen.dart new file mode 100644 index 000000000..455237836 --- /dev/null +++ b/lib/view/oscilloscope_screen.dart @@ -0,0 +1,98 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/view/widgets/channel_parameters_widget.dart'; +import 'package:pslab/view/widgets/common_scaffold_widget.dart'; +import 'package:pslab/view/widgets/data_analysis_widget.dart'; +import 'package:pslab/view/widgets/oscilloscope_screen_tabs.dart'; +import 'package:pslab/view/widgets/timebase_trigger_widget.dart'; +import 'package:pslab/view/widgets/xyplot_widget.dart'; + +import '../providers/oscilloscope_state_provider.dart'; + +class OscilloscopeScreen extends StatefulWidget { + const OscilloscopeScreen({super.key}); + + @override + State createState() => _OscilloscopeScreenState(); +} + +class _OscilloscopeScreenState extends State { + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + _setLandscapeOrientation(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + }); + super.initState(); + } + + void _setLandscapeOrientation() { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => OscilloscopeStateProvider()), + ], + child: SafeArea( + child: CommonScaffold( + title: 'Oscilloscope', + body: Container( + margin: const EdgeInsets.only(left: 5, top: 5), + child: Row( + children: [ + Container( + width: 310.w, + margin: const EdgeInsets.only(right: 5), + child: Column( + children: [ + SizedBox( + height: 380.h, + child: LineChart( + LineChartData( + backgroundColor: Colors.black, + titlesData: const FlTitlesData(show: false), + borderData: FlBorderData(show: false), + ), + ), + ), + Expanded( + child: Consumer( + builder: (context, provider, _) { + switch (provider.selectedIndex) { + case 0: + return const ChannelParametersWidget(); + case 1: + return const TimebaseTriggerWidget(); + case 2: + return const DataAnalysisWidget(); // Replace with your widget for Tab 3 + case 3: + return const XYPlotWidget(); + default: + return const ChannelParametersWidget(); + } + }, + ), + ), + ], + ), + ), + const Expanded( + child: OscilloscopeScreenTabs(), + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/view/widgets/applications_list_item.dart b/lib/view/widgets/applications_list_item.dart index 3f7ffc0dd..728b2e43e 100644 --- a/lib/view/widgets/applications_list_item.dart +++ b/lib/view/widgets/applications_list_item.dart @@ -16,96 +16,91 @@ class ApplicationsListItem extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - /**/ - }, - child: Card( - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - elevation: 2, - child: Container( - height: 225, - decoration: BoxDecoration( - color: const Color(0xFFD32F2F), - borderRadius: BorderRadius.circular(5)), - child: Stack( - children: [ - Positioned( - top: 0, - bottom: 10, - right: 10, - child: Column( - children: [ - Expanded( - child: Image.asset( - verticalBarsIcon, - width: 100, - fit: BoxFit.fill, - ), - ), - const SizedBox( - height: 100, + return Card( + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + elevation: 2, + child: Container( + height: 225, + decoration: BoxDecoration( + color: const Color(0xFFD32F2F), + borderRadius: BorderRadius.circular(5)), + child: Stack( + children: [ + Positioned( + top: 0, + bottom: 10, + right: 10, + child: Column( + children: [ + Expanded( + child: Image.asset( + verticalBarsIcon, width: 100, - ) - ], - ), - ), - Positioned( - right: 10, - bottom: 10, - left: 0, - child: Row( - children: [ - Expanded( - child: Image.asset( - horizontalBarsIcon, - height: 100, - fit: BoxFit.fill, - ), + fit: BoxFit.fill, ), - SizedBox( + ), + const SizedBox( + height: 100, + width: 100, + ) + ], + ), + ), + Positioned( + right: 10, + bottom: 10, + left: 0, + child: Row( + children: [ + Expanded( + child: Image.asset( + horizontalBarsIcon, height: 100, - width: 100, - child: Image.asset( - instrumentIcon, - fit: BoxFit.fill, - ), - ) - ], - ), + fit: BoxFit.fill, + ), + ), + SizedBox( + height: 100, + width: 100, + child: Image.asset( + instrumentIcon, + fit: BoxFit.fill, + ), + ) + ], ), - Positioned( - top: 20, - left: 20, - right: 100, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - heading, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - textAlign: TextAlign.start, + ), + Positioned( + top: 20, + left: 20, + right: 100, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + heading, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, ), - Text( - description, - style: const TextStyle( - fontSize: 16, - color: Colors.white, - ), - textAlign: TextAlign.start, + textAlign: TextAlign.start, + ), + Text( + description, + style: const TextStyle( + fontSize: 16, + color: Colors.white, ), - ], - ), + textAlign: TextAlign.start, + ), + ], ), - ], - ), + ), + ], ), ), ); diff --git a/lib/view/widgets/channel_parameters_widget.dart b/lib/view/widgets/channel_parameters_widget.dart new file mode 100644 index 000000000..a37892acd --- /dev/null +++ b/lib/view/widgets/channel_parameters_widget.dart @@ -0,0 +1,313 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class ChannelParametersWidget extends StatefulWidget { + const ChannelParametersWidget({super.key}); + + @override + State createState() => _ChannelParametersState(); +} + +class _ChannelParametersState extends State { + bool? isCH1Selected = false; + bool? isCH2Selected = false; + bool? isCH3Selected = false; + bool? isMICSelected = false; + bool? isInBuiltMICSelected = false; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(top: 5, bottom: 5), + decoration: BoxDecoration( + border: Border.all(width: 1, color: const Color(0xFFD32F2F)), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + Positioned( + top: 0.h, + left: 2.w, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: isCH1Selected, + activeColor: const Color(0xFFCE525F), + onChanged: (bool? value) { + setState( + () { + isCH1Selected = value; + }, + ); + }, + ), + Padding( + padding: EdgeInsets.only(top: 2.h), + child: const Text( + 'CH1', + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontSize: 15, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 1.h, left: 3.w), + child: const Text( + 'Range', + style: TextStyle( + color: Color(0xFF424242), + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + fontSize: 14, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 1.h, left: 4.w), + child: DropdownMenu( + initialSelection: '+/-16V', + width: 60.w, + dropdownMenuEntries: [ + '+/-16V', + '+/-8V', + '+/-4V', + '+/-3V', + '+/-2V', + '+/-1.5V', + '+/-1V', + '+/-500mV', + '+/-160V', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 0.h), + child: DropdownMenu( + width: 50.w, + initialSelection: 'CH1', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + 'RES', + 'VOL', + 'CAP', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 15, + ), + ), + ), + ], + ), + ), + Positioned( + left: 2.w, + bottom: 8.h, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: isCH2Selected, + activeColor: const Color(0xFFCE525F), + onChanged: (bool? value) { + setState( + () { + isCH2Selected = value; + }, + ); + }, + ), + Padding( + padding: EdgeInsets.only(top: 2.h), + child: const Text( + 'CH2', + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontSize: 15, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 1.h, left: 3.w), + child: const Text( + 'Range', + style: TextStyle( + color: Color(0xFF424242), + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + fontSize: 14, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 2.h, left: 4.w), + child: SizedBox( + width: 60.w, + child: const Text( + '+/-16V', + style: TextStyle( + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontSize: 14, + ), + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 2.h), + child: SizedBox( + width: 45.w, + child: const Text( + 'CH2', + style: TextStyle( + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontSize: 15, + ), + ), + ), + ), + ], + ), + ), + Positioned( + top: 16.h, + right: 4.w, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: isCH3Selected, + activeColor: const Color(0xFFCE525F), + onChanged: (bool? value) { + setState( + () { + isCH3Selected = value; + }, + ); + }, + ), + Padding( + padding: EdgeInsets.only(top: 2.h), + child: const Text( + 'CH3 (+/- 3.3V)', + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontSize: 15, + ), + ), + ), + ], + ), + ), + Positioned( + bottom: 8.h, + right: 4.w, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Radio( + activeColor: const Color(0xFFCE525F), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: true, + groupValue: isInBuiltMICSelected, + toggleable: true, + onChanged: (bool? value) { + setState( + () { + if (value == null) { + isInBuiltMICSelected = false; + } else { + isInBuiltMICSelected = value; + isMICSelected = !value; + } + }, + ); + }, + ), + Padding( + padding: EdgeInsets.only(top: 2.h), + child: const Text( + 'In-Built MIC', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + ), + ), + ), + Radio( + activeColor: const Color(0xFFCE525F), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: true, + groupValue: isMICSelected, + toggleable: true, + onChanged: (bool? value) { + setState( + () { + if (value == null) { + isMICSelected = false; + } else { + isMICSelected = value; + isInBuiltMICSelected = !value; + } + }, + ); + }, + ), + Padding( + padding: EdgeInsets.only(top: 2.h), + child: const Text( + 'PSLab MIC', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/view/widgets/common_scaffold_widget.dart b/lib/view/widgets/common_scaffold_widget.dart index d4ce62245..adcec7b19 100644 --- a/lib/view/widgets/common_scaffold_widget.dart +++ b/lib/view/widgets/common_scaffold_widget.dart @@ -1,23 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'navigation_drawer.dart'; - class CommonScaffold extends StatefulWidget { final String title; final Widget body; final Key? scaffoldKey; - final int index; final List? actions; - final String icUsbDisconnected = 'assets/icons/ic_usb_disconnected.png'; - const CommonScaffold( - {super.key, - required this.body, - required this.title, - this.scaffoldKey, - this.actions, - required this.index}); + const CommonScaffold({ + super.key, + required this.body, + required this.title, + this.scaffoldKey, + this.actions, + }); @override State createState() => _CommonScaffoldState(); @@ -35,10 +31,19 @@ class _CommonScaffoldState extends State { leading: Builder(builder: (context) { return IconButton( onPressed: () { - Scaffold.of(context).openDrawer(); + if (Navigator.canPop(context) && + ModalRoute.of(context)?.settings.name == '/') { + Navigator.popUntil(context, ModalRoute.withName('/')); + } else { + Navigator.pushNamedAndRemoveUntil( + context, + '/', + (route) => route.isFirst, + ); + } }, icon: const Icon( - Icons.menu, + Icons.arrow_back, color: Colors.white, ), ); @@ -49,35 +54,14 @@ class _CommonScaffoldState extends State { widget.title, style: const TextStyle( color: Colors.white, - fontSize: 19, + fontSize: 15, ), ), actions: [ - IconButton( - icon: Image.asset( - widget.icUsbDisconnected, - width: 24, - height: 24, - ), - onPressed: () { - /**/ - }, - ), - IconButton( - icon: const Icon( - Icons.more_vert, - color: Colors.white, - ), - onPressed: () { - /**/ - }, - ), + if (widget.actions != null) ...widget.actions!, ], ), body: widget.body, - drawer: NavDrawer( - selectedIndex: widget.index, - ), ); } } diff --git a/lib/view/widgets/data_analysis_widget.dart b/lib/view/widgets/data_analysis_widget.dart new file mode 100644 index 000000000..23dd06d33 --- /dev/null +++ b/lib/view/widgets/data_analysis_widget.dart @@ -0,0 +1,276 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class DataAnalysisWidget extends StatefulWidget { + const DataAnalysisWidget({super.key}); + + @override + State createState() => _DataAnalysisState(); +} + +class _DataAnalysisState extends State { + bool? isFourierTransformSelected = false; + double horizontalOffset = 0; + double verticalOffset = 0; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Container( + margin: const EdgeInsets.only(top: 5, bottom: 5, right: 2.5), + decoration: BoxDecoration( + border: Border.all(width: 1, color: const Color(0xFFD32F2F)), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + Positioned( + top: 0.h, + left: 2.w, + right: 0.h, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Checkbox( + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + activeColor: const Color(0xFFCE525F), + value: isFourierTransformSelected, + onChanged: (bool? value) { + setState( + () { + isFourierTransformSelected = value; + }, + ); + }, + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: const Text( + 'Fourier Transforms', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), + ), + ), + const Spacer(), + DropdownMenu( + width: 45.w, + initialSelection: 'CH1', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ], + ), + ), + Positioned( + bottom: 0.h, + left: 6.w, + right: 0.h, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + DropdownMenu( + initialSelection: 'Sine Fit', + dropdownMenuEntries: [ + 'Sine Fit', + 'Square Fit', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + const Spacer(), + Padding( + padding: EdgeInsets.only(left: 4.w), + child: DropdownMenu( + width: 45.w, + initialSelection: 'CH1', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.only(top: 5, bottom: 5, left: 2.5), + decoration: BoxDecoration( + border: Border.all(width: 1, color: const Color(0xFFD32F2F)), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + Positioned( + bottom: 0.h, + top: 0.h, + left: 6.w, + child: Center( + child: DropdownMenu( + width: 50.w, + initialSelection: 'CH1', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ), + ), + Positioned( + top: 4.h, + right: 4.w, + left: 45.w, + child: Row( + children: [ + Expanded( + child: SliderTheme( + data: const SliderThemeData( + trackHeight: 1, + thumbShape: + RoundSliderThumbShape(enabledThumbRadius: 6), + ), + child: Slider( + activeColor: const Color(0xFFCE525F), + min: -16, + max: 16, + value: verticalOffset, + onChanged: (double value) { + setState( + () { + verticalOffset = value; + }, + ); + }, + ), + ), + ), + Text( + '${verticalOffset.toStringAsFixed(2)} V', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), + ), + ], + ), + ), + Positioned( + bottom: 0.h, + right: 4.w, + left: 45.w, + child: Row( + children: [ + Expanded( + child: SliderTheme( + data: const SliderThemeData( + trackHeight: 1, + thumbShape: + RoundSliderThumbShape(enabledThumbRadius: 6), + ), + child: Slider( + activeColor: const Color(0xFFCE525F), + min: 0, + max: 1, + value: horizontalOffset, + onChanged: (double value) { + setState( + () { + horizontalOffset = value; + }, + ); + }, + ), + ), + ), + Text( + '${horizontalOffset.toStringAsFixed(2)} ms', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), + ), + ], + ), + ) + ], + ), + ), + ) + ], + ); + } +} diff --git a/lib/view/widgets/main_scaffold_widget.dart b/lib/view/widgets/main_scaffold_widget.dart new file mode 100644 index 000000000..9e50738dc --- /dev/null +++ b/lib/view/widgets/main_scaffold_widget.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'navigation_drawer.dart'; + +class MainScaffold extends StatefulWidget { + final String title; + final Widget body; + final Key? scaffoldKey; + final int index; + final List? actions; + final String icUsbDisconnected = 'assets/icons/ic_usb_disconnected.png'; + + const MainScaffold( + {super.key, + required this.body, + required this.title, + this.scaffoldKey, + this.actions, + required this.index}); + + @override + State createState() => _MainScaffoldState(); +} + +class _MainScaffoldState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + resizeToAvoidBottomInset: true, + appBar: AppBar( + systemOverlayStyle: + const SystemUiOverlayStyle(statusBarColor: Color(0xFFD32F2F)), + leading: Builder(builder: (context) { + return IconButton( + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + icon: const Icon( + Icons.menu, + color: Colors.white, + ), + ); + }), + backgroundColor: const Color(0xFFD32F2F), + title: Text( + key: widget.scaffoldKey, + widget.title, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + actions: [ + IconButton( + icon: Image.asset( + widget.icUsbDisconnected, + width: 24, + height: 24, + ), + onPressed: () { + /**/ + }, + ), + IconButton( + icon: const Icon( + Icons.more_vert, + color: Colors.white, + ), + onPressed: () { + /**/ + }, + ), + ], + ), + body: widget.body, + drawer: NavDrawer( + selectedIndex: widget.index, + ), + ); + } +} diff --git a/lib/view/widgets/navigation_drawer.dart b/lib/view/widgets/navigation_drawer.dart index 8568dbcf6..01b867b22 100644 --- a/lib/view/widgets/navigation_drawer.dart +++ b/lib/view/widgets/navigation_drawer.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:google_fonts/google_fonts.dart'; class NavDrawer extends StatefulWidget { final int selectedIndex; @@ -39,17 +38,17 @@ class _NavDrawerState extends State { fit: BoxFit.contain, ), ), - Padding( - padding: const EdgeInsets.only(top: 16), + const Padding( + padding: EdgeInsets.only(top: 16), child: Text('Not Connected', - style: GoogleFonts.roboto( - fontSize: 14, - )), + style: TextStyle( + fontSize: 14, fontStyle: FontStyle.normal)), ), ], ), ), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -81,6 +80,7 @@ class _NavDrawerState extends State { }, ), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -101,6 +101,7 @@ class _NavDrawerState extends State { ), const Divider(), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -120,6 +121,7 @@ class _NavDrawerState extends State { }, ), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -139,6 +141,7 @@ class _NavDrawerState extends State { }, ), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -159,6 +162,7 @@ class _NavDrawerState extends State { ), const Divider(), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -178,6 +182,7 @@ class _NavDrawerState extends State { }, ), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -197,6 +202,7 @@ class _NavDrawerState extends State { }, ), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -216,6 +222,7 @@ class _NavDrawerState extends State { }, ), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -235,6 +242,7 @@ class _NavDrawerState extends State { }, ), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -254,6 +262,7 @@ class _NavDrawerState extends State { }, ), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -273,6 +282,7 @@ class _NavDrawerState extends State { }, ), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -292,6 +302,7 @@ class _NavDrawerState extends State { }, ), ListTile( + minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( diff --git a/lib/view/widgets/oscilloscope_screen_tabs.dart b/lib/view/widgets/oscilloscope_screen_tabs.dart new file mode 100644 index 000000000..16c8dd1e9 --- /dev/null +++ b/lib/view/widgets/oscilloscope_screen_tabs.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/providers/oscilloscope_state_provider.dart'; + +class OscilloscopeScreenTabs extends StatefulWidget { + final String channelParametersImage = 'assets/images/channel_parameters.gif'; + final String dataAnalysisImage = 'assets/images/data_analysis.png'; + final String timebaseTriggerImage = 'assets/images/timebase.png'; + final String xyPlotImage = 'assets/images/xymode.png'; + + const OscilloscopeScreenTabs({super.key}); + + @override + State createState() => _OscilloscopeTabsState(); +} + +class _OscilloscopeTabsState extends State { + @override + Widget build(BuildContext context) { + OscilloscopeStateProvider oscilloscopeStateProvider = + Provider.of(context); + return Container( + margin: const EdgeInsets.only(right: 5, bottom: 5), + decoration: BoxDecoration( + border: Border.all(width: 3, color: const Color(0xFFB1BCBE)), + color: const Color(0xFF7A7A7A), + borderRadius: BorderRadius.circular(10), + ), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: GestureDetector( + onTap: () { + setState( + () { + oscilloscopeStateProvider.updateSelectedIndex(0); + }, + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 4, horizontal: 8), + decoration: BoxDecoration( + color: const Color(0xF3DBDBDB), + borderRadius: BorderRadius.circular(2), + ), + margin: const EdgeInsets.all(4), + child: Image.asset( + widget.channelParametersImage, + ), + ), + ), + Container( + margin: + const EdgeInsets.symmetric(vertical: 4, horizontal: 2), + color: oscilloscopeStateProvider.selectedIndex == 0 + ? const Color(0xFFC72C2C) + : Colors.transparent, + child: const Text( + 'Channels', + textAlign: TextAlign.center, + maxLines: 1, + style: TextStyle( + color: Colors.black, + fontSize: 9, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + ), + ), + ), + const Divider( + color: Color(0xFFB1BCBE), + height: 2, + ) + ], + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState( + () { + oscilloscopeStateProvider.updateSelectedIndex(1); + }, + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 4, horizontal: 8), + decoration: BoxDecoration( + color: const Color(0xF3DBDBDB), + borderRadius: BorderRadius.circular(2), + ), + margin: const EdgeInsets.all(4), + child: Image.asset( + widget.timebaseTriggerImage, + ), + ), + ), + Container( + margin: const EdgeInsets.symmetric( + vertical: 4, horizontal: 2), + color: oscilloscopeStateProvider.selectedIndex == 1 + ? const Color(0xFFC72C2C) + : Colors.transparent, + child: const Text( + 'Timebase', + textAlign: TextAlign.center, + maxLines: 1, + style: TextStyle( + color: Colors.black, + fontSize: 9, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + ), + )), + const Divider( + color: Color(0xFFB1BCBE), + height: 2, + ) + ], + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState( + () { + oscilloscopeStateProvider.updateSelectedIndex(2); + }, + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 4, horizontal: 8), + decoration: BoxDecoration( + color: const Color(0xF3DBDBDB), + borderRadius: BorderRadius.circular(2), + ), + margin: const EdgeInsets.all(4), + child: Image.asset( + widget.dataAnalysisImage, + ), + ), + ), + Container( + margin: const EdgeInsets.symmetric( + vertical: 4, horizontal: 2), + color: oscilloscopeStateProvider.selectedIndex == 2 + ? const Color(0xFFC72C2C) + : Colors.transparent, + child: const Text( + 'Data Analysis', + textAlign: TextAlign.center, + maxLines: 1, + style: TextStyle( + color: Colors.black, + fontSize: 9, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + ), + )), + const Divider( + color: Color(0xFFB1BCBE), + height: 2, + ) + ], + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState( + () { + oscilloscopeStateProvider.updateSelectedIndex(3); + }, + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 4, horizontal: 8), + decoration: BoxDecoration( + color: const Color(0xF3DBDBDB), + borderRadius: BorderRadius.circular(2), + ), + margin: const EdgeInsets.all(4), + child: Image.asset( + widget.xyPlotImage, + ), + ), + ), + Container( + margin: + const EdgeInsets.symmetric(vertical: 4, horizontal: 2), + color: oscilloscopeStateProvider.selectedIndex == 3 + ? const Color(0xFFC72C2C) + : Colors.transparent, + child: const Text( + 'XY Plot', + textAlign: TextAlign.center, + maxLines: 1, + style: TextStyle( + color: Colors.black, + fontSize: 9, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/view/widgets/timebase_trigger_widget.dart b/lib/view/widgets/timebase_trigger_widget.dart new file mode 100644 index 000000000..7179b762f --- /dev/null +++ b/lib/view/widgets/timebase_trigger_widget.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class TimebaseTriggerWidget extends StatefulWidget { + const TimebaseTriggerWidget({super.key}); + + @override + State createState() => _TimebaseTriggerState(); +} + +class _TimebaseTriggerState extends State { + double _timebaseSlider = 0; + double triggerValue = 0; + bool? isTriggerChecked = false; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(top: 5, bottom: 5), + decoration: BoxDecoration( + border: Border.all(width: 1, color: const Color(0xFFD32F2F)), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + Positioned( + top: 0.h, + left: 2.w, + right: 0.w, + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + activeColor: const Color(0xFFCE525F), + value: isTriggerChecked, + onChanged: (bool? value) { + setState( + () { + isTriggerChecked = value; + }, + ); + }, + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: const Text( + 'Trigger', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + ), + ), + ), + Padding( + padding: EdgeInsets.only(left: 4.w), + child: DropdownMenu( + width: 50.w, + initialSelection: 'CH1', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ), + Expanded( + child: SliderTheme( + data: const SliderThemeData( + trackHeight: 1, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6), + ), + child: Slider( + activeColor: const Color(0xFFCE525F), + min: -16, + max: 16, + value: triggerValue, + onChanged: (double value) { + setState( + () { + triggerValue = value; + }, + ); + }, + ), + ), + ), + Text( + '${triggerValue.toStringAsFixed(1)} V', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), + ), + Padding( + padding: EdgeInsets.only(left: 8.w), + child: DropdownMenu( + width: 70.w, + initialSelection: 'Rising Edge', + dropdownMenuEntries: [ + 'Rising Edge', + 'Falling Edge', + 'Dual Edge', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ), + ], + ), + ), + Positioned( + bottom: 0.h, + left: 8.w, + right: 8.w, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Timebase', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + ), + ), + Expanded( + child: SliderTheme( + data: const SliderThemeData( + trackHeight: 1, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6), + ), + child: Slider( + activeColor: const Color(0xFFCE525F), + min: 0, + max: 8, + divisions: 8, + value: _timebaseSlider, + onChanged: (double value) { + setState( + () { + _timebaseSlider = value; + }, + ); + }, + ), + ), + ), + Text( + '${_timebaseSlider.toStringAsFixed(2)} ms', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/view/widgets/xyplot_widget.dart b/lib/view/widgets/xyplot_widget.dart new file mode 100644 index 000000000..d1b4e7d86 --- /dev/null +++ b/lib/view/widgets/xyplot_widget.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class XYPlotWidget extends StatefulWidget { + const XYPlotWidget({super.key}); + + @override + State createState() => _XYPlotState(); +} + +class _XYPlotState extends State { + bool? isXYPlotSelected = false; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(top: 5, bottom: 5), + decoration: BoxDecoration( + border: Border.all(width: 1, color: const Color(0xFFD32F2F)), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + Positioned( + top: 0.h, + left: 2.w, + right: 0.w, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + activeColor: const Color(0xFFCE525F), + value: isXYPlotSelected, + onChanged: (bool? value) { + setState( + () { + isXYPlotSelected = value; + }, + ); + }, + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: const Text( + 'Enable XY Plot', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), + ), + ), + const Spacer(), + DropdownMenu( + width: 50.w, + initialSelection: 'CH1', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + DropdownMenu( + width: 50.w, + initialSelection: 'CH2', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 114527ff4..7bc7df166 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -81,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "10ddaf334fe84d59333a12d153043e366f243e0bdfff2df0313e1e249f5bf926" + url: "https://pub.dev" + source: hosted + version: "0.70.1" flutter: dependency: "direct main" description: flutter @@ -106,10 +122,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.17" flutter_test: dependency: "direct dev" description: flutter @@ -135,10 +151,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" leak_tracker: dependency: transitive description: @@ -195,6 +211,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -283,6 +307,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" sky_engine: dependency: transitive description: flutter @@ -356,10 +388,10 @@ packages: dependency: transitive description: name: vector_graphics_codec - sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.12" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a5b7018c5..25ac38acb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,8 +36,10 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 flutter_screenutil: ^5.9.3 - flutter_svg: ^2.0.16 - google_fonts: 6.2.1 + flutter_svg: ^2.0.17 + google_fonts: ^6.2.1 + fl_chart: ^0.70.1 + provider: ^6.1.2 dev_dependencies: flutter_test: @@ -64,6 +66,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/icons/ + - assets/images/ # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg From aba81ed9e83cf3ab263ee4b00f6ceba22da62974 Mon Sep 17 00:00:00 2001 From: Anashuman Singh Date: Sat, 18 Jan 2025 23:25:29 +0530 Subject: [PATCH 2/4] feat: improved layouts --- .../widgets/channel_parameters_widget.dart | 555 +++++++++--------- lib/view/widgets/data_analysis_widget.dart | 471 ++++++++------- lib/view/widgets/timebase_trigger_widget.dart | 343 ++++++----- lib/view/widgets/xyplot_widget.dart | 199 ++++--- 4 files changed, 848 insertions(+), 720 deletions(-) diff --git a/lib/view/widgets/channel_parameters_widget.dart b/lib/view/widgets/channel_parameters_widget.dart index a37892acd..1f3517075 100644 --- a/lib/view/widgets/channel_parameters_widget.dart +++ b/lib/view/widgets/channel_parameters_widget.dart @@ -17,297 +17,322 @@ class _ChannelParametersState extends State { @override Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(top: 5, bottom: 5), - decoration: BoxDecoration( - border: Border.all(width: 1, color: const Color(0xFFD32F2F)), - borderRadius: BorderRadius.circular(10), - ), - child: Stack( - children: [ - Positioned( - top: 0.h, - left: 2.w, - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: isCH1Selected, - activeColor: const Color(0xFFCE525F), - onChanged: (bool? value) { - setState( - () { - isCH1Selected = value; + return Stack( + children: [ + Container( + margin: const EdgeInsets.only(top: 8, bottom: 5), + decoration: BoxDecoration( + border: Border.all(width: 1, color: const Color(0xFFD32F2F)), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + Positioned( + top: 0.h, + left: 2.w, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: isCH1Selected, + activeColor: const Color(0xFFCE525F), + onChanged: (bool? value) { + setState( + () { + isCH1Selected = value; + }, + ); }, - ); - }, - ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: const Text( - 'CH1', - style: TextStyle( - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, - fontSize: 15, ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 1.h, left: 3.w), - child: const Text( - 'Range', - style: TextStyle( - color: Color(0xFF424242), - fontWeight: FontWeight.normal, - fontStyle: FontStyle.normal, - fontSize: 14, + Padding( + padding: EdgeInsets.only(top: 2.h), + child: const Text( + 'CH1', + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontSize: 15, + ), + ), ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 1.h, left: 4.w), - child: DropdownMenu( - initialSelection: '+/-16V', - width: 60.w, - dropdownMenuEntries: [ - '+/-16V', - '+/-8V', - '+/-4V', - '+/-3V', - '+/-2V', - '+/-1.5V', - '+/-1V', - '+/-500mV', - '+/-160V', - ].map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, - ); - }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, + Padding( + padding: EdgeInsets.only(top: 1.h, left: 3.w), + child: const Text( + 'Range', + style: TextStyle( + color: Color(0xFF424242), + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + fontSize: 14, + ), + ), ), - textStyle: const TextStyle( - fontSize: 14, + Padding( + padding: EdgeInsets.only(top: 1.h, left: 4.w), + child: DropdownMenu( + initialSelection: '+/-16V', + width: 60.w, + dropdownMenuEntries: [ + '+/-16V', + '+/-8V', + '+/-4V', + '+/-3V', + '+/-2V', + '+/-1.5V', + '+/-1V', + '+/-500mV', + '+/-160V', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), ), - ), + Padding( + padding: EdgeInsets.only(top: 0.h), + child: DropdownMenu( + width: 50.w, + initialSelection: 'CH1', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + 'RES', + 'VOL', + 'CAP', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 15, + ), + ), + ), + ], ), - Padding( - padding: EdgeInsets.only(top: 0.h), - child: DropdownMenu( - width: 50.w, - initialSelection: 'CH1', - dropdownMenuEntries: [ - 'CH1', - 'CH2', - 'CH3', - 'MIC', - 'RES', - 'VOL', - 'CAP', - ].map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, + ), + Positioned( + left: 2.w, + bottom: 8.h, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: isCH2Selected, + activeColor: const Color(0xFFCE525F), + onChanged: (bool? value) { + setState( + () { + isCH2Selected = value; + }, ); }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, - ), - textStyle: const TextStyle( - fontSize: 15, ), - ), - ), - ], - ), - ), - Positioned( - left: 2.w, - bottom: 8.h, - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: isCH2Selected, - activeColor: const Color(0xFFCE525F), - onChanged: (bool? value) { - setState( - () { - isCH2Selected = value; - }, - ); - }, - ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: const Text( - 'CH2', - style: TextStyle( - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, - fontSize: 15, + Padding( + padding: EdgeInsets.only(top: 2.h), + child: const Text( + 'CH2', + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontSize: 15, + ), + ), ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 1.h, left: 3.w), - child: const Text( - 'Range', - style: TextStyle( - color: Color(0xFF424242), - fontWeight: FontWeight.normal, - fontStyle: FontStyle.normal, - fontSize: 14, + Padding( + padding: EdgeInsets.only(top: 1.h, left: 3.w), + child: const Text( + 'Range', + style: TextStyle( + color: Color(0xFF424242), + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + fontSize: 14, + ), + ), ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 2.h, left: 4.w), - child: SizedBox( - width: 60.w, - child: const Text( - '+/-16V', - style: TextStyle( - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 14, + Padding( + padding: EdgeInsets.only(top: 2.h, left: 4.w), + child: SizedBox( + width: 60.w, + child: const Text( + '+/-16V', + style: TextStyle( + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontSize: 14, + ), + ), ), ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: SizedBox( - width: 45.w, - child: const Text( - 'CH2', - style: TextStyle( - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 15, + Padding( + padding: EdgeInsets.only(top: 2.h), + child: SizedBox( + width: 45.w, + child: const Text( + 'CH2', + style: TextStyle( + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontSize: 15, + ), + ), ), ), - ), + ], ), - ], - ), - ), - Positioned( - top: 16.h, - right: 4.w, - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: isCH3Selected, - activeColor: const Color(0xFFCE525F), - onChanged: (bool? value) { - setState( - () { - isCH3Selected = value; + ), + Positioned( + top: 16.h, + right: 4.w, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: isCH3Selected, + activeColor: const Color(0xFFCE525F), + onChanged: (bool? value) { + setState( + () { + isCH3Selected = value; + }, + ); }, - ); - }, - ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: const Text( - 'CH3 (+/- 3.3V)', - style: TextStyle( - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, - fontSize: 15, ), - ), + Padding( + padding: EdgeInsets.only(top: 2.h), + child: const Text( + 'CH3 (+/- 3.3V)', + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontSize: 15, + ), + ), + ), + ], ), - ], - ), - ), - Positioned( - bottom: 8.h, - right: 4.w, - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Radio( - activeColor: const Color(0xFFCE525F), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: true, - groupValue: isInBuiltMICSelected, - toggleable: true, - onChanged: (bool? value) { - setState( - () { - if (value == null) { - isInBuiltMICSelected = false; - } else { - isInBuiltMICSelected = value; - isMICSelected = !value; - } + ), + Positioned( + bottom: 8.h, + right: 4.w, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Radio( + activeColor: const Color(0xFFCE525F), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: true, + groupValue: isInBuiltMICSelected, + toggleable: true, + onChanged: (bool? value) { + setState( + () { + if (value == null) { + isInBuiltMICSelected = false; + } else { + isInBuiltMICSelected = value; + isMICSelected = !value; + } + }, + ); }, - ); - }, - ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: const Text( - 'In-Built MIC', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, ), - ), - ), - Radio( - activeColor: const Color(0xFFCE525F), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: true, - groupValue: isMICSelected, - toggleable: true, - onChanged: (bool? value) { - setState( - () { - if (value == null) { - isMICSelected = false; - } else { - isMICSelected = value; - isInBuiltMICSelected = !value; - } + Padding( + padding: EdgeInsets.only(top: 2.h), + child: const Text( + 'In-Built MIC', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + ), + ), + ), + Radio( + activeColor: const Color(0xFFCE525F), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: true, + groupValue: isMICSelected, + toggleable: true, + onChanged: (bool? value) { + setState( + () { + if (value == null) { + isMICSelected = false; + } else { + isMICSelected = value; + isInBuiltMICSelected = !value; + } + }, + ); }, - ); - }, - ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: const Text( - 'PSLab MIC', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, ), - ), + Padding( + padding: EdgeInsets.only(top: 2.h), + child: const Text( + 'PSLab MIC', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + ), + ), + ), + ], + ), + ), + ], + ), + ), + Positioned( + left: 0.w, + right: 0.w, + top: 2.h, + child: Align( + alignment: Alignment.center, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 2.w), + decoration: const BoxDecoration(color: Colors.white), + child: const Text( + 'Channel Parameters', + style: TextStyle( + color: Color(0xFFC72C2C), + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + fontSize: 13, ), - ], + ), ), ), - ], - ), + ) + ], ); } } diff --git a/lib/view/widgets/data_analysis_widget.dart b/lib/view/widgets/data_analysis_widget.dart index 23dd06d33..4231bcf3d 100644 --- a/lib/view/widgets/data_analysis_widget.dart +++ b/lib/view/widgets/data_analysis_widget.dart @@ -19,107 +19,180 @@ class _DataAnalysisState extends State { mainAxisSize: MainAxisSize.max, children: [ Expanded( - child: Container( - margin: const EdgeInsets.only(top: 5, bottom: 5, right: 2.5), - decoration: BoxDecoration( - border: Border.all(width: 1, color: const Color(0xFFD32F2F)), - borderRadius: BorderRadius.circular(10), - ), - child: Stack( - children: [ - Positioned( - top: 0.h, - left: 2.w, - right: 0.h, - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: Checkbox( - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - activeColor: const Color(0xFFCE525F), - value: isFourierTransformSelected, - onChanged: (bool? value) { - setState( - () { - isFourierTransformSelected = value; + child: Stack( + children: [ + Container( + margin: const EdgeInsets.only(top: 8, bottom: 5, right: 2.5), + decoration: BoxDecoration( + border: Border.all(width: 1, color: const Color(0xFFD32F2F)), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + Positioned( + top: 0.h, + left: 2.w, + right: 0.h, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Checkbox( + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + activeColor: const Color(0xFFCE525F), + value: isFourierTransformSelected, + onChanged: (bool? value) { + setState( + () { + isFourierTransformSelected = value; + }, + ); }, - ); - }, - ), + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: const Text( + 'Fourier Analysis', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), + ), + ), + const Spacer(), + DropdownMenu( + width: 50.w, + initialSelection: 'CH1', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ], ), - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: const Text( - 'Fourier Transforms', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - fontStyle: FontStyle.normal, + ), + Positioned( + bottom: 0.h, + left: 6.w, + right: 0.h, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + DropdownMenu( + width: 80.w, + initialSelection: 'Sine Fit', + dropdownMenuEntries: [ + 'Sine Fit', + 'Square Fit', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), ), - ), + const Spacer(), + Padding( + padding: EdgeInsets.only(left: 4.w), + child: DropdownMenu( + width: 50.w, + initialSelection: 'CH1', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ), + ], ), - const Spacer(), - DropdownMenu( - width: 45.w, - initialSelection: 'CH1', - dropdownMenuEntries: [ - 'CH1', - 'CH2', - 'CH3', - 'MIC', - ].map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, - ); - }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, - ), - textStyle: const TextStyle( - fontSize: 14, - ), + ), + ], + ), + ), + Positioned( + left: 0.w, + right: 0.w, + top: 2.h, + child: Align( + alignment: Alignment.center, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 2.w), + decoration: const BoxDecoration(color: Colors.white), + child: const Text( + 'Data Analysis', + style: TextStyle( + color: Color(0xFFC72C2C), + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + fontSize: 13, ), - ], + ), ), ), - Positioned( - bottom: 0.h, - left: 6.w, - right: 0.h, - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - DropdownMenu( - initialSelection: 'Sine Fit', - dropdownMenuEntries: [ - 'Sine Fit', - 'Square Fit', - ].map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, - ); - }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, - ), - textStyle: const TextStyle( - fontSize: 14, - ), - ), - const Spacer(), - Padding( - padding: EdgeInsets.only(left: 4.w), + ) + ], + ), + ), + Expanded( + child: Stack( + children: [ + Container( + margin: const EdgeInsets.only(top: 8, bottom: 5, left: 2.5), + decoration: BoxDecoration( + border: Border.all(width: 1, color: const Color(0xFFD32F2F)), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + Positioned( + bottom: 0.h, + top: 0.h, + left: 6.w, + child: Center( child: DropdownMenu( - width: 45.w, + width: 50.w, initialSelection: 'CH1', dropdownMenuEntries: [ 'CH1', @@ -142,132 +215,110 @@ class _DataAnalysisState extends State { ), ), ), - ], - ), - ), - ], - ), - ), - ), - Expanded( - child: Container( - margin: const EdgeInsets.only(top: 5, bottom: 5, left: 2.5), - decoration: BoxDecoration( - border: Border.all(width: 1, color: const Color(0xFFD32F2F)), - borderRadius: BorderRadius.circular(10), - ), - child: Stack( - children: [ - Positioned( - bottom: 0.h, - top: 0.h, - left: 6.w, - child: Center( - child: DropdownMenu( - width: 50.w, - initialSelection: 'CH1', - dropdownMenuEntries: [ - 'CH1', - 'CH2', - 'CH3', - 'MIC', - ].map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, - ); - }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, - ), - textStyle: const TextStyle( - fontSize: 14, - ), ), - ), - ), - Positioned( - top: 4.h, - right: 4.w, - left: 45.w, - child: Row( - children: [ - Expanded( - child: SliderTheme( - data: const SliderThemeData( - trackHeight: 1, - thumbShape: - RoundSliderThumbShape(enabledThumbRadius: 6), - ), - child: Slider( - activeColor: const Color(0xFFCE525F), - min: -16, - max: 16, - value: verticalOffset, - onChanged: (double value) { - setState( - () { - verticalOffset = value; + Positioned( + top: 4.h, + right: 4.w, + left: 45.w, + child: Row( + children: [ + Expanded( + child: SliderTheme( + data: const SliderThemeData( + trackHeight: 1, + thumbShape: RoundSliderThumbShape( + enabledThumbRadius: 6), + ), + child: Slider( + activeColor: const Color(0xFFCE525F), + min: -16, + max: 16, + value: verticalOffset, + onChanged: (double value) { + setState( + () { + verticalOffset = value; + }, + ); }, - ); - }, + ), + ), ), - ), - ), - Text( - '${verticalOffset.toStringAsFixed(2)} V', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - fontStyle: FontStyle.normal, - ), - ), - ], - ), - ), - Positioned( - bottom: 0.h, - right: 4.w, - left: 45.w, - child: Row( - children: [ - Expanded( - child: SliderTheme( - data: const SliderThemeData( - trackHeight: 1, - thumbShape: - RoundSliderThumbShape(enabledThumbRadius: 6), + Text( + '${verticalOffset.toStringAsFixed(2)} V', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), ), - child: Slider( - activeColor: const Color(0xFFCE525F), - min: 0, - max: 1, - value: horizontalOffset, - onChanged: (double value) { - setState( - () { - horizontalOffset = value; + ], + ), + ), + Positioned( + bottom: 0.h, + right: 4.w, + left: 45.w, + child: Row( + children: [ + Expanded( + child: SliderTheme( + data: const SliderThemeData( + trackHeight: 1, + thumbShape: RoundSliderThumbShape( + enabledThumbRadius: 6), + ), + child: Slider( + activeColor: const Color(0xFFCE525F), + min: 0, + max: 1, + value: horizontalOffset, + onChanged: (double value) { + setState( + () { + horizontalOffset = value; + }, + ); }, - ); - }, + ), + ), ), - ), + Text( + '${horizontalOffset.toStringAsFixed(2)} ms', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), + ), + ], ), - Text( - '${horizontalOffset.toStringAsFixed(2)} ms', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - fontStyle: FontStyle.normal, - ), + ) + ], + ), + ), + Positioned( + left: 0.w, + right: 0.w, + top: 2.h, + child: Align( + alignment: Alignment.center, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 2.w), + decoration: const BoxDecoration(color: Colors.white), + child: const Text( + 'Offsets', + style: TextStyle( + color: Color(0xFFC72C2C), + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + fontSize: 13, ), - ], + ), ), - ) - ], - ), + ), + ) + ], ), ) ], diff --git a/lib/view/widgets/timebase_trigger_widget.dart b/lib/view/widgets/timebase_trigger_widget.dart index 7179b762f..7f65ae91d 100644 --- a/lib/view/widgets/timebase_trigger_widget.dart +++ b/lib/view/widgets/timebase_trigger_widget.dart @@ -15,180 +15,207 @@ class _TimebaseTriggerState extends State { @override Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(top: 5, bottom: 5), - decoration: BoxDecoration( - border: Border.all(width: 1, color: const Color(0xFFD32F2F)), - borderRadius: BorderRadius.circular(10), - ), - child: Stack( - children: [ - Positioned( - top: 0.h, - left: 2.w, - right: 0.w, - child: Row( - children: [ - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - activeColor: const Color(0xFFCE525F), - value: isTriggerChecked, - onChanged: (bool? value) { - setState( - () { - isTriggerChecked = value; + return Stack( + children: [ + Container( + margin: const EdgeInsets.only(top: 8, bottom: 5), + decoration: BoxDecoration( + border: Border.all(width: 1, color: const Color(0xFFD32F2F)), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + Positioned( + top: 0.h, + left: 2.w, + right: 0.w, + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + activeColor: const Color(0xFFCE525F), + value: isTriggerChecked, + onChanged: (bool? value) { + setState( + () { + isTriggerChecked = value; + }, + ); }, - ); - }, - ), - ), - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: const Text( - 'Trigger', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, - ), - ), - ), - Padding( - padding: EdgeInsets.only(left: 4.w), - child: DropdownMenu( - width: 50.w, - initialSelection: 'CH1', - dropdownMenuEntries: [ - 'CH1', - 'CH2', - 'CH3', - 'MIC', - ].map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, - ); - }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, + ), ), - textStyle: const TextStyle( - fontSize: 14, + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: const Text( + 'Trigger', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + ), + ), ), - ), - ), - Expanded( - child: SliderTheme( - data: const SliderThemeData( - trackHeight: 1, - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6), + Padding( + padding: EdgeInsets.only(left: 8.w), + child: DropdownMenu( + width: 50.w, + initialSelection: 'CH1', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), ), - child: Slider( - activeColor: const Color(0xFFCE525F), - min: -16, - max: 16, - value: triggerValue, - onChanged: (double value) { - setState( - () { - triggerValue = value; + Expanded( + child: SliderTheme( + data: const SliderThemeData( + trackHeight: 1, + thumbShape: + RoundSliderThumbShape(enabledThumbRadius: 6), + ), + child: Slider( + activeColor: const Color(0xFFCE525F), + min: -16, + max: 16, + value: triggerValue, + onChanged: (double value) { + setState( + () { + triggerValue = value; + }, + ); }, - ); - }, + ), + ), ), - ), - ), - Text( - '${triggerValue.toStringAsFixed(1)} V', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - fontStyle: FontStyle.normal, - ), - ), - Padding( - padding: EdgeInsets.only(left: 8.w), - child: DropdownMenu( - width: 70.w, - initialSelection: 'Rising Edge', - dropdownMenuEntries: [ - 'Rising Edge', - 'Falling Edge', - 'Dual Edge', - ].map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, - ); - }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, + Text( + '${triggerValue.toStringAsFixed(1)} V', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), ), - textStyle: const TextStyle( - fontSize: 14, + Padding( + padding: EdgeInsets.only(left: 8.w), + child: DropdownMenu( + width: 70.w, + initialSelection: 'Rising Edge', + dropdownMenuEntries: [ + 'Rising Edge', + 'Falling Edge', + 'Dual Edge', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), ), - ), + ], ), - ], - ), - ), - Positioned( - bottom: 0.h, - left: 8.w, - right: 8.w, - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text( - 'Timebase', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, - ), - ), - Expanded( - child: SliderTheme( - data: const SliderThemeData( - trackHeight: 1, - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6), + ), + Positioned( + bottom: 0.h, + left: 8.w, + right: 8.w, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Timebase', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + ), ), - child: Slider( - activeColor: const Color(0xFFCE525F), - min: 0, - max: 8, - divisions: 8, - value: _timebaseSlider, - onChanged: (double value) { - setState( - () { - _timebaseSlider = value; + Expanded( + child: SliderTheme( + data: const SliderThemeData( + trackHeight: 1, + thumbShape: + RoundSliderThumbShape(enabledThumbRadius: 6), + ), + child: Slider( + activeColor: const Color(0xFFCE525F), + min: 0, + max: 8, + divisions: 8, + value: _timebaseSlider, + onChanged: (double value) { + setState( + () { + _timebaseSlider = value; + }, + ); }, - ); - }, + ), + ), + ), + Text( + '${_timebaseSlider.toStringAsFixed(2)} ms', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), ), - ), + ], ), - Text( - '${_timebaseSlider.toStringAsFixed(2)} ms', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - fontStyle: FontStyle.normal, - ), + ), + ], + ), + ), + Positioned( + left: 0.w, + right: 0.w, + top: 2.h, + child: Align( + alignment: Alignment.center, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 2.w), + decoration: const BoxDecoration(color: Colors.white), + child: const Text( + 'Timebase & Trigger', + style: TextStyle( + color: Color(0xFFC72C2C), + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + fontSize: 13, ), - ], + ), ), ), - ], - ), + ) + ], ); } } diff --git a/lib/view/widgets/xyplot_widget.dart b/lib/view/widgets/xyplot_widget.dart index d1b4e7d86..d5d9082e6 100644 --- a/lib/view/widgets/xyplot_widget.dart +++ b/lib/view/widgets/xyplot_widget.dart @@ -13,99 +13,124 @@ class _XYPlotState extends State { @override Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(top: 5, bottom: 5), - decoration: BoxDecoration( - border: Border.all(width: 1, color: const Color(0xFFD32F2F)), - borderRadius: BorderRadius.circular(10), - ), - child: Stack( - children: [ - Positioned( - top: 0.h, - left: 2.w, - right: 0.w, - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - activeColor: const Color(0xFFCE525F), - value: isXYPlotSelected, - onChanged: (bool? value) { - setState( - () { - isXYPlotSelected = value; + return Stack( + children: [ + Container( + margin: const EdgeInsets.only(top: 8, bottom: 5), + decoration: BoxDecoration( + border: Border.all(width: 1, color: const Color(0xFFD32F2F)), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + Positioned( + top: 0.h, + left: 2.w, + right: 0.w, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + activeColor: const Color(0xFFCE525F), + value: isXYPlotSelected, + onChanged: (bool? value) { + setState( + () { + isXYPlotSelected = value; + }, + ); }, - ); - }, - ), - ), - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: const Text( - 'Enable XY Plot', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - fontStyle: FontStyle.normal, + ), ), - ), - ), - const Spacer(), - DropdownMenu( - width: 50.w, - initialSelection: 'CH1', - dropdownMenuEntries: [ - 'CH1', - 'CH2', - 'CH3', - 'MIC', - ].map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, - ); - }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, - ), - textStyle: const TextStyle( - fontSize: 14, - ), + Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: const Text( + 'Enable XY Plot', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), + ), + ), + const Spacer(), + DropdownMenu( + width: 50.w, + initialSelection: 'CH1', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + DropdownMenu( + width: 50.w, + initialSelection: 'CH2', + dropdownMenuEntries: [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ], ), - DropdownMenu( - width: 50.w, - initialSelection: 'CH2', - dropdownMenuEntries: [ - 'CH1', - 'CH2', - 'CH3', - 'MIC', - ].map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, - ); - }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, - ), - textStyle: const TextStyle( - fontSize: 14, - ), + ), + ], + ), + ), + Positioned( + left: 0.w, + right: 0.w, + top: 2.h, + child: Align( + alignment: Alignment.center, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 2.w), + decoration: const BoxDecoration(color: Colors.white), + child: const Text( + 'XY Plot', + style: TextStyle( + color: Color(0xFFC72C2C), + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + fontSize: 13, ), - ], + ), ), ), - ], - ), + ) + ], ); } } From 903372836bd31f05f8d918b70408e514a2c27379 Mon Sep 17 00:00:00 2001 From: Anashuman Singh Date: Wed, 22 Jan 2025 00:14:48 +0530 Subject: [PATCH 3/4] feat: added basic communication and initialization logic --- analysis_options.yaml | 10 +- android/app/src/main/AndroidManifest.xml | 4 + .../app/src/main/res/xml/device_filter.xml | 39 +++ assets/icons/ic_usb_connected.png | Bin 0 -> 9195 bytes .../analog_acquisition_channel.dart | 58 ++++ .../analogChannel/analog_constants.dart | 43 +++ .../analogChannel/analog_input_source.dart | 86 ++++++ lib/communication/commands_proto.dart | 208 ++++++++++++++ lib/communication/communication_handler.dart | 83 ++++++ .../digitalChannel/digital_channel.dart | 164 +++++++++++ lib/communication/packet_handler.dart | 155 +++++++++++ lib/communication/science_lab.dart | 263 ++++++++++++++++++ lib/constants.dart | 29 ++ lib/main.dart | 16 +- lib/others/science_lab_common.dart | 25 ++ lib/providers/board_state_provider.dart | 55 ++++ lib/providers/locator.dart | 8 + .../oscilloscope_state_provider.dart | 9 + .../widgets/channel_parameters_widget.dart | 112 +++++--- lib/view/widgets/main_scaffold_widget.dart | 25 +- lib/view/widgets/navigation_drawer.dart | 18 +- pubspec.lock | 57 ++-- pubspec.yaml | 6 + 23 files changed, 1399 insertions(+), 74 deletions(-) create mode 100644 android/app/src/main/res/xml/device_filter.xml create mode 100644 assets/icons/ic_usb_connected.png create mode 100644 lib/communication/analogChannel/analog_acquisition_channel.dart create mode 100644 lib/communication/analogChannel/analog_constants.dart create mode 100644 lib/communication/analogChannel/analog_input_source.dart create mode 100644 lib/communication/commands_proto.dart create mode 100644 lib/communication/communication_handler.dart create mode 100644 lib/communication/digitalChannel/digital_channel.dart create mode 100644 lib/communication/packet_handler.dart create mode 100644 lib/communication/science_lab.dart create mode 100644 lib/others/science_lab_common.dart create mode 100644 lib/providers/board_state_provider.dart create mode 100644 lib/providers/locator.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d2902135..5c77047e3 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -21,8 +21,14 @@ linter: # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + avoid_print: false + constant_identifier_names: false + non_constant_identifier_names: false + prefer_final_fields: false + unnecessary_nullable_for_final_variable_declarations: false # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options +analyzer: + errors: + unused_element: ignore diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9fe1b9a8b..b2077ed1f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + + + diff --git a/android/app/src/main/res/xml/device_filter.xml b/android/app/src/main/res/xml/device_filter.xml new file mode 100644 index 000000000..03290f8c0 --- /dev/null +++ b/android/app/src/main/res/xml/device_filter.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/ic_usb_connected.png b/assets/icons/ic_usb_connected.png new file mode 100644 index 0000000000000000000000000000000000000000..8d7d5a889b52a1911a9574b21dc15619c901288a GIT binary patch literal 9195 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 _xAxis = List.filled(10000, 0.0); + List yAxis = List.filled(10000, 0.0); + + AnalogAcquisitionChannel(String channel) { + _calibration_ref196 = 1; + _resolution = 10; + length = 100; + _timebase = 1; + _analogInputSource = AnalogInputSource('CH1'); + } + + List fixValue(List val) { + List calcData = List.filled(val.length, 0.0); + if (_resolution == 12) { + for (int i = 0; i < val.length; i++) { + calcData[i] = _calibration_ref196 * + (_analogInputSource.calPoly12.evaluate(val[i])); + } + } else { + for (int i = 0; i < val.length; i++) { + calcData[i] = _calibration_ref196 * + (_analogInputSource.calPoly10.evaluate(val[i])); + } + } + return calcData; + } + + void setParams(String? channel, int length, double timebase, int resolution, + AnalogInputSource? source, double? gain) { + _analogInputSource = source!; + if (resolution != -1) _resolution = resolution; + if (length != -1) this.length = length; + if (timebase != -1) _timebase = timebase; + regenerateXAxis(); + } + + void regenerateXAxis() { + for (int i = 0; i < length; i++) { + _xAxis[i] = _timebase * i; + } + } + + List getXAxis() { + return List.from(_xAxis.getRange(0, length)); + } + + List getYAxis() { + return List.from(yAxis.getRange(0, length)); + } +} diff --git a/lib/communication/analogChannel/analog_constants.dart b/lib/communication/analogChannel/analog_constants.dart new file mode 100644 index 000000000..2da08d5d4 --- /dev/null +++ b/lib/communication/analogChannel/analog_constants.dart @@ -0,0 +1,43 @@ +class AnalogConstants { + List gains = [1, 2, 4, 5, 8, 10, 16, 32, 1 / 11]; + List allAnalogChannels = [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + 'CAP', + 'RES', + 'VOL', + ]; + List biPolars = [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ]; + Map> inputRanges = {}; + Map picADCMultiplex = {}; + + AnalogConstants() { + inputRanges = { + "CH1": [16.5, -16.5], + "CH2": [16.5, -16.5], + "CH3": [-3.3, 3.3], + "MIC": [-3.3, 3.3], + "CAP": [0, 3.3], + "RES": [0, 3.3], + "VOL": [0, 3.3], + }; + + picADCMultiplex = { + "CH1": 3, + "CH2": 0, + "CH3": 1, + "MIC": 2, + "AN4": 4, + "RES": 7, + "CAP": 5, + "VOL": 8, + }; + } +} diff --git a/lib/communication/analogChannel/analog_input_source.dart b/lib/communication/analogChannel/analog_input_source.dart new file mode 100644 index 000000000..f56c9f3e7 --- /dev/null +++ b/lib/communication/analogChannel/analog_input_source.dart @@ -0,0 +1,86 @@ +import 'package:polynomial/polynomial.dart'; +import 'package:pslab/communication/analogChannel/analog_constants.dart'; + +class AnalogInputSource { + late List _gainValues, _range; + bool gainEnabled = false, inverted = false, calibrationReady = false; + double _gain = 0; + late int gainPGA, CHOSA; + final String _channelName; + late Polynomial calPoly10; + late Polynomial calPoly12; + late Polynomial voltToCode10; + late Polynomial voltToCode12; + List _adc_shifts = []; + List _polynomials = []; + + AnalogInputSource(this._channelName) { + AnalogConstants analogConstants = AnalogConstants(); + _range = analogConstants.inputRanges[_channelName]!; + _gainValues = analogConstants.gains; + CHOSA = analogConstants.picADCMultiplex[_channelName]!; + + calPoly10 = Polynomial([0, 3.3 / 1023, 0]); + calPoly12 = Polynomial([0, 3.3 / 4095, 0]); + + if (_range[1] - _range[0] < 0) { + inverted = true; + } + if (_channelName == "CH1") { + gainEnabled = true; + gainPGA = 1; + _gain = 0; + } else if (_channelName == "CH2") { + gainEnabled = true; + gainPGA = 2; + _gain = 0; + } + _gain = 0; + regenerateCalibration(); + } + + bool setGain(int index) { + if (!gainEnabled) { + print("$_channelName: Analog gain is not available"); + return false; + } + _gain = _gainValues[index]; + regenerateCalibration(); + return true; + } + + void ignoreCalibration() { + calibrationReady = false; + } + + void regenerateCalibration() { + double A, B, intercept, slope; + B = _range[1]; + A = _range[0]; + if (_gain >= 0 && _gain <= 8) { + _gain = _gainValues[_gain.round()]; + B /= _gain; + A /= _gain; + } + slope = 2 * (B - A); + intercept = 2 * A; + if (!calibrationReady || _gain == 8) { + calPoly10 = Polynomial([intercept, slope / 1023, 0]); + calPoly12 = Polynomial([intercept, slope / 4095, 0]); + } + + voltToCode10 = Polynomial([-1023 * intercept / slope, 1023 / slope, 0]); + voltToCode12 = Polynomial([-4095 * intercept / slope, 4095, 0]); + } + + List cal12(List raw) { + List calcData = List.filled(raw.length, 0.0); + for (int i = 0; i < raw.length; i++) { + double avgShifts = + (_adc_shifts[raw[i].floor()] + _adc_shifts[raw[i].ceil()]) / 2; + raw[i] -= 4095 * avgShifts / 3.3; + calcData[i] = _polynomials[_gain as int].evaluate(raw[i]); + } + return calcData; + } +} diff --git a/lib/communication/commands_proto.dart b/lib/communication/commands_proto.dart new file mode 100644 index 000000000..810813af4 --- /dev/null +++ b/lib/communication/commands_proto.dart @@ -0,0 +1,208 @@ +class CommandsProto { + int ACKNOWLEDGE = 254; + int MAX_SAMPLES = 10000; + int DATA_SPLITTING = 60; + + int FLASH = 1; + int READ_FLASH = 1; + int WRITE_FLASH = 2; + int WRITE_BULK_FLASH = 3; + int READ_BULK_FLASH = 4; + + int ADC = 2; + int CAPTURE_ONE = 1; + int CAPTURE_TWO = 2; + int CAPTURE_DMASPEED = 3; + int CAPTURE_FOUR = 4; + int CONFIGURE_TRIGGER = 5; + int GET_CAPTURE_STATUS = 6; + int GET_CAPTURE_CHANNEL = 7; + int SET_PGA_GAIN = 8; + int GET_VOLTAGE = 9; + int GET_VOLTAGE_SUMMED = 10; + int START_ADC_STREAMING = 11; + int SELECT_PGA_CHANNEL = 12; + int CAPTURE_12BIT = 13; + int CAPTURE_MULTIPLE = 14; + int SET_HI_CAPTURE = 15; + int SET_LO_CAPTURE = 16; + + int MULTIPOINT_CAPACITANCE = 20; + int SET_CAP = 21; + int PULSE_TRAIN = 22; + + int SPI_HEADER = 3; + int START_SPI = 1; + int SEND_SPI8 = 2; + int SEND_SPI16 = 3; + int STOP_SPI = 4; + int SET_SPI_PARAMETERS = 5; + int SEND_SPI8_BURST = 6; + int SEND_SPI16_BURST = 7; + + int I2C_HEADER = 4; + int I2C_START = 1; + int I2C_SEND = 2; + int I2C_STOP = 3; + int I2C_RESTART = 4; + int I2C_READ_END = 5; + int I2C_READ_MORE = 6; + int I2C_WAIT = 7; + int I2C_SEND_BURST = 8; + int I2C_CONFIG = 9; + int I2C_STATUS = 10; + int I2C_READ_BULK = 11; + int I2C_WRITE_BULK = 12; + int I2C_ENABLE_SMBUS = 13; + int I2C_INIT = 14; + int I2C_PULLDOWN_SCL = 15; + int I2C_DISABLE_SMBUS = 16; + int I2C_START_SCOPE = 17; + + int UART_2 = 5; + int SEND_BYTE = 1; + int SEND_INT = 2; + int SEND_ADDRESS = 3; + int SET_BAUD = 4; + int SET_MODE = 5; + int READ_BYTE = 6; + int READ_INT = 7; + int READ_UART2_STATUS = 8; + + int DAC = 6; + int SET_DAC = 1; + int SET_CALIBRATED_DAC = 2; + + int WAVEGEN = 7; + int SET_WG = 1; + int SET_SQR1 = 3; + int SET_SQR2 = 4; + int SET_SQRS = 5; + int TUNE_SINE_OSCILLATOR = 6; + int SQR4 = 7; + int MAP_REFERENCE = 8; + int SET_BOTH_WG = 9; + int SET_WAVEFORM_TYPE = 10; + int SELECT_FREQ_REGISTER = 11; + int DELAY_GENERATOR = 12; + int SET_SINE1 = 13; + int SET_SINE2 = 14; + + int LOAD_WAVEFORM1 = 15; + int LOAD_WAVEFORM2 = 16; + int SQR1_PATTERN = 17; + + int DOUT = 8; + int SET_STATE = 1; + + int DIN = 9; + int GET_STATE = 1; + int GET_STATES = 2; + + int LA1 = 0; + int LA2 = 1; + int LA3 = 2; + int LA4 = 3; + int LMETER = 4; + + int TIMING = 10; + int GET_TIMING = 1; + int GET_PULSE_TIME = 2; + int GET_DUTY_CYCLE = 3; + int START_ONE_CHAN_LA = 4; + int START_TWO_CHAN_LA = 5; + int START_FOUR_CHAN_LA = 6; + int FETCH_DMA_DATA = 7; + int FETCH_INT_DMA_DATA = 8; + int FETCH_LONG_DMA_DATA = 9; + int GET_LA_PROGRESS = 10; + int GET_INITIAL_DIGITAL_STATES = 11; + + int TIMING_MEASUREMENTS = 12; + int INTERVAL_MEASUREMENTS = 13; + int CONFIGURE_COMPARATOR = 14; + int START_ALTERNATE_ONE_CHAN_LA = 15; + int START_THREE_CHAN_LA = 16; + int STOP_LA = 17; + + int COMMON = 11; + + int GET_CTMU_VOLTAGE = 1; + int GET_CAPACITANCE = 2; + int GET_FREQUENCY = 3; + int GET_INDUCTANCE = 4; + + int GET_VERSION = 5; + + int RETRIEVE_BUFFER = 8; + int GET_HIGH_FREQUENCY = 9; + int CLEAR_BUFFER = 10; + int SET_RGB1 = 11; + int READ_PROGRAM_ADDRESS = 12; + int WRITE_PROGRAM_ADDRESS = 13; + int READ_DATA_ADDRESS = 14; + int WRITE_DATA_ADDRESS = 15; + + int GET_CAP_RANGE = 16; + int SET_RGB2 = 17; + int READ_LOG = 18; + int RESTORE_STANDALONE = 19; + int GET_ALTERNATE_HIGH_FREQUENCY = 20; + int SET_RGB3 = 22; + + int START_CTMU = 23; + int STOP_CTMU = 24; + + int START_COUNTING = 25; + int FETCH_COUNT = 26; + int FILL_BUFFER = 27; + + int SETBAUD = 12; + int BAUD9600 = 1; + int BAUD14400 = 2; + int BAUD19200 = 3; + int BAUD28800 = 4; + int BAUD38400 = 5; + int BAUD57600 = 6; + int BAUD115200 = 7; + int BAUD230400 = 8; + int BAUD1000000 = 9; + + int NRFL01 = 13; + int NRF_SETUP = 1; + int NRF_RXMODE = 2; + int NRF_TXMODE = 3; + int NRF_POWER_DOWN = 4; + int NRF_RXCHAR = 5; + int NRF_TXCHAR = 6; + int NRF_HASDATA = 7; + int NRF_FLUSH = 8; + int NRF_WRITEREG = 9; + int NRF_READREG = 10; + int NRF_GETSTATUS = 11; + int NRF_WRITECOMMAND = 12; + int NRF_WRITEPAYLOAD = 13; + int NRF_READPAYLOAD = 14; + int NRF_WRITEADDRESS = 15; + int NRF_TRANSACTION = 16; + int NRF_START_TOKEN_MANAGER = 17; + int NRF_STOP_TOKEN_MANAGER = 18; + int NRF_TOTAL_TOKENS = 19; + int NRF_REPORTS = 20; + int NRF_WRITE_REPORT = 21; + int NRF_DELETE_REPORT_ROW = 22; + + int NRF_WRITEADDRESSES = 23; + + int NONSTANDARD_IO = 14; + int HX711_HEADER = 1; + int HCSR04_HEADER = 2; + int AM2302_HEADER = 3; + int TCD1304_HEADER = 4; + int STEPPER_MOTOR = 5; + + int PASSTHROUGHS = 15; + int PASS_UART = 1; + + int STOP_STREAMING = 253; +} diff --git a/lib/communication/communication_handler.dart b/lib/communication/communication_handler.dart new file mode 100644 index 000000000..90b5283ab --- /dev/null +++ b/lib/communication/communication_handler.dart @@ -0,0 +1,83 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:usb_serial/usb_serial.dart'; + +class CommunicationHandler { + static const int PSLAB_VENDOR_ID_V5 = 1240; + static const int PSLAB_PRODUCT_ID_V5 = 223; + static const int PSLAB_VENDOR_ID_V6 = 0x10C4; + static const int PSLAB_PRODUCT_ID_V6 = 0xEA60; + UsbDevice? mDevice; + UsbPort? mPort; + bool connected = false; + bool deviceFound = false; + + Future initialize() async { + List devices = await UsbSerial.listDevices(); + for (UsbDevice device in devices) { + if ((device.vid == PSLAB_VENDOR_ID_V5 && + device.pid == PSLAB_PRODUCT_ID_V5) || + (device.vid == PSLAB_VENDOR_ID_V6 && + device.pid == PSLAB_PRODUCT_ID_V6)) { + mDevice = device; + deviceFound = true; + break; + } + } + if (deviceFound) { + print("Found PSLab device"); + } else { + print("No drivers found"); + } + } + + Future open() async { + if (!deviceFound) { + throw Exception("Device not connected"); + } + mPort = await mDevice!.create(); + + bool openResult = await mPort!.open(); + if (!openResult) { + throw Exception("Failed to open"); + } + await mPort!.setPortParameters( + 1000000, + UsbPort.DATABITS_8, + UsbPort.STOPBITS_1, + UsbPort.PARITY_NONE, + ); + mPort!.inputStream!.listen((_) => ()); + connected = true; + } + + bool isDeviceFound() => deviceFound; + + bool isConnected() => connected; + + void close() { + if (!connected || mPort == null) return; + mPort?.close(); + connected = false; + } + + Future read(Uint8List dest, int bytesToRead, int timeoutMillis) async { + int numBytesRead = 0; + await for (Uint8List receivedData + in mPort!.inputStream!.timeout(Duration(milliseconds: timeoutMillis))) { + dest.setRange(0, receivedData.length, receivedData); + if (receivedData.length == bytesToRead) { + print("Read: $bytesToRead"); + } else { + print("Read Error: ${bytesToRead - numBytesRead}"); + } + return numBytesRead; + } + return numBytesRead; + } + + void write(Uint8List src, int timeoutMillis) { + mPort!.write(src); + } +} diff --git a/lib/communication/digitalChannel/digital_channel.dart b/lib/communication/digitalChannel/digital_channel.dart new file mode 100644 index 000000000..ab8495016 --- /dev/null +++ b/lib/communication/digitalChannel/digital_channel.dart @@ -0,0 +1,164 @@ +import 'dart:collection'; + +class DigitalChannel { + static const int EVERY_EDGE = 1; + static const int DISABLED = 0; + static const int EVERY_SIXTEENTH_RISING_EDGE = 5; + static const int EVERY_FOURTH_RISING_EDGE = 4; + static const int EVERY_RISING_EDGE = 3; + static const int EVERY_FALLING_EDGE = 2; + static List digitalChannelNames = [ + 'LA1', + 'LA2', + 'LA3', + 'LA4', + 'RES', + 'EXT', + 'FRQ' + ]; + late String channelName, dataType; + late int initialStateOverride, + channelNumber, + length, + prescaler, + trigger, + dLength, + plotLength, + maxTime, + mode; + late List xAxis, yAxis, timestamps; + late bool initialState; + late double gain, maxT; + + DigitalChannel(this.channelNumber) { + channelName = digitalChannelNames[channelNumber]; + gain = 0; + xAxis = List.filled(20000, 0.0); + yAxis = List.filled(20000, 0.0); + timestamps = List.filled(10000, 0.0); + length = 100; + initialState = false; + prescaler = 0; + dataType = 'int'; + trigger = 0; + dLength = 0; + plotLength = 0; + maxT = 0; + maxTime = 0; + initialStateOverride = 0; + mode = EVERY_EDGE; + } + + void loadData( + LinkedHashMap initialStates, List timestamps) { + if (initialStateOverride != 0) { + initialState = (initialStateOverride - 1) == 1; + initialStateOverride = 0; + } else { + final int? s = initialStates[channelName]!; + initialState = s != null && s == 1; + } + this.timestamps.setRange(0, timestamps.length, timestamps); + dLength = timestamps.length; + double factor; + switch (prescaler) { + case 0: + factor = 64; + break; + case 1: + factor = 8; + break; + case 2: + factor = 4; + break; + default: + factor = 1; + break; + } + for (int i = 0; i < this.timestamps.length; i++) { + this.timestamps[i] /= factor; + } + if (dLength > 0) { + maxT = this.timestamps[this.timestamps.length - 1]; + } else { + maxT = 0; + } + } + + void generateAxes() { + int HIGH = 1, LOW = 0, state; + if (initialState) { + state = LOW; + } else { + state = HIGH; + } + + if (mode == DISABLED) { + xAxis[0] = 0; + yAxis[0] = 0; + plotLength = 1; + } else if (mode == EVERY_EDGE) { + xAxis[0] = 0; + yAxis[0] = state as double; + int length = 0; + for (int i = 1, j = 1; i < dLength; i++, j++) { + xAxis[j] = timestamps[i]; + yAxis[j] = state as double; + if (state == HIGH) { + state = LOW; + } else { + state = HIGH; + } + j++; + xAxis[j] = timestamps[i]; + yAxis[j] = state as double; + length = j; + } + plotLength = length; + } else if (mode == EVERY_FALLING_EDGE) { + xAxis[0] = 0; + yAxis[0] = HIGH as double; + int length = 0; + for (int i = 1, j = 1; i < dLength; i++, j++) { + xAxis[j] = timestamps[i]; + yAxis[j] = HIGH as double; + j++; + xAxis[j] = timestamps[i]; + yAxis[j] = LOW as double; + j++; + xAxis[j] = timestamps[i]; + yAxis[j] = HIGH as double; + length = j; + } + state = HIGH; + plotLength = length; + } else if (mode == EVERY_RISING_EDGE || + mode == EVERY_FOURTH_RISING_EDGE || + mode == EVERY_SIXTEENTH_RISING_EDGE) { + xAxis[0] = 0; + yAxis[0] = LOW as double; + int length = 0; + for (int i = 1, j = 1; i < dLength; i++, j++) { + xAxis[j] = timestamps[i]; + yAxis[j] = LOW as double; + j++; + xAxis[j] = timestamps[i]; + yAxis[j] = HIGH as double; + j++; + xAxis[j] = timestamps[i]; + yAxis[j] = LOW as double; + length = j; + } + state = LOW; + plotLength = length; + } + + List getXAxis() { + return List.from(xAxis.getRange(0, plotLength)); + } + + List getYAxis() { + return List.from(yAxis.getRange(0, plotLength)); + } + } +} diff --git a/lib/communication/packet_handler.dart b/lib/communication/packet_handler.dart new file mode 100644 index 000000000..fdad6244e --- /dev/null +++ b/lib/communication/packet_handler.dart @@ -0,0 +1,155 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:pslab/communication/commands_proto.dart'; +import 'package:pslab/communication/communication_handler.dart'; + +class PacketHandler { + Uint8List _buffer = Uint8List(10000); + late bool _connected; + late CommunicationHandler _mCommunicationHandler; + static String version = ''; + late CommandsProto _mCommandsProto; + int _timeout = 500, VERSION_STRING_LENGTH = 8; + + PacketHandler(int timeout, CommunicationHandler communicationHandler) { + _connected = false; + _timeout = timeout; + _mCommandsProto = CommandsProto(); + _mCommunicationHandler = communicationHandler; + _connected = (_mCommunicationHandler.isConnected()); + } + + bool isConnected() { + _connected = (_mCommunicationHandler.isConnected()); + return _connected; + } + + Future getVersion() async { + try { + sendByte(_mCommandsProto.COMMON); + sendByte(_mCommandsProto.GET_VERSION); + await _commonRead(VERSION_STRING_LENGTH + 1); + version = utf8.decode(_buffer).split('\n').first; + } catch (e) { + print("Error in getting version: $e"); + } + return version; + } + + void sendByte(int val) { + if (!isConnected()) { + throw Exception("Device not connected"); + } + try { + _commonWrite(Uint8List.fromList([val & 0xFF])); + } catch (e) { + print("Error in sending byte: $e"); + } + } + + void sendInt(int val) { + if (!isConnected()) { + throw Exception("Device not connected"); + } + try { + _commonWrite(Uint8List.fromList([val & 0xFF, (val >> 8) & 0xFF])); + } catch (e) { + print("Error in sending int: $e"); + } + } + + Future getAcknowledgement() async { + try { + await _commonRead(1); + return _buffer[0]; + } catch (e) { + print(e); + return 3; + } + } + + Future getByte() async { + try { + int numBytesRead = await _commonRead(3); + if (numBytesRead == 3) { + return _buffer[0]; + } else { + print("Error in getting voltage"); + } + } catch (e) { + print(e); + } + return -1; + } + + Future getVoltageSummation() async { + try { + int numBytesRead = await _commonRead(3); + if (numBytesRead == 3) { + return (_buffer[0] & 0xFF | ((_buffer[1] << 8) & 0xFF00)); + } else { + print("Error in getting voltage"); + } + } catch (e) { + print(e); + } + return -1; + } + + Future getInt() async { + try { + int numBytesRead = await _commonRead(2); + if (numBytesRead == 2) { + return (_buffer[0] & 0xFF | ((_buffer[1] << 8) & 0xFF00)); + } else { + print("Error in reading Int"); + } + } catch (e) { + print(e); + } + return -1; + } + + Future getLong() async { + try { + int numBytesRead = await _commonRead(4); + if (numBytesRead == 4) { + return _buffer.buffer.asByteData(0, 4).getInt32(0, Endian.little); + } else { + print("Error in reading Long"); + } + } catch (e) { + print(e); + } + return -1; + } + + Future read(Uint8List dest, int bytesToRead) async { + int numBytesRead = await _commonRead(bytesToRead); + for (int i = 0; i < bytesToRead; i++) { + dest[i] = _buffer[i]; + } + if (numBytesRead == bytesToRead) { + return numBytesRead; + } else { + print("Error in PacketHandler Reading"); + } + return -1; + } + + Future _commonRead(int bytesToRead) async { + final List bytesRead = [0]; + if (_mCommunicationHandler.isConnected()) { + bytesRead[0] = + await _mCommunicationHandler.read(_buffer, bytesToRead, _timeout); + } + return bytesRead[0]; + } + + void _commonWrite(Uint8List data) { + if (_mCommunicationHandler.isConnected()) { + _mCommunicationHandler.write(data, _timeout); + } + } +} diff --git a/lib/communication/science_lab.dart b/lib/communication/science_lab.dart new file mode 100644 index 000000000..242eb550e --- /dev/null +++ b/lib/communication/science_lab.dart @@ -0,0 +1,263 @@ +import 'dart:math'; + +import 'package:pslab/communication/commands_proto.dart'; +import 'package:pslab/communication/communication_handler.dart'; +import 'package:pslab/communication/packet_handler.dart'; + +import 'analogChannel/analog_acquisition_channel.dart'; +import 'analogChannel/analog_constants.dart'; +import 'analogChannel/analog_input_source.dart'; +import 'digitalChannel/digital_channel.dart'; + +class ScienceLab { + late int DDS_CLOCK, + MAX_SAMPLES, + samples, + triggerLevel, + triggerChannel, + errorCount, + channelsInBuffer, + digitalChannelsInBuffer, + dataSplitting; + late double sin1Frequency, sin2Frequency; + late List currents, currentScalars, gainValues, buffer; + late double SOCKET_CAPACITANCE, resistanceScaling, timebase; + late bool streaming, calibrated = false; + late List allAnalogChannels, allDigitalChannels; + Map analogInputSources = {}; + Map squareWaveFrequency = {}; + Map gains = {}; + Map waveType = {}; + List aChannels = []; + List dChannels = []; + + late CommunicationHandler mCommunicationHandler; + late PacketHandler mPacketHandler; + late CommandsProto mCommandsProto; + late AnalogConstants mAnalogConstants; + + ScienceLab(CommunicationHandler communicationHandler) { + mCommunicationHandler = communicationHandler; + mCommandsProto = CommandsProto(); + mAnalogConstants = AnalogConstants(); + } + + Future connect() async { + if (isDeviceFound()) { + try { + await mCommunicationHandler.open(); + mPacketHandler = PacketHandler(500, mCommunicationHandler); + } catch (e) { + print(e); + } + } + if (isConnected()) { + await _initializeVariables(); + } + } + + bool isConnected() { + return mCommunicationHandler.isConnected(); + } + + bool isDeviceFound() { + return mCommunicationHandler.isDeviceFound(); + } + + Future getVersion() async { + if (isConnected()) { + return await mPacketHandler.getVersion(); + } else { + return 'Not Connected'; + } + } + + Future _initializeVariables() async { + DDS_CLOCK = 0; + timebase = 40; + MAX_SAMPLES = mCommandsProto.MAX_SAMPLES; + samples = MAX_SAMPLES; + triggerChannel = 0; + triggerLevel = 550; + errorCount = 0; + channelsInBuffer = 0; + digitalChannelsInBuffer = 0; + currents = [0.55e-3, 0.55e-6, 0.55e-5, 0.55e-4]; + currentScalars = [1.0, 1.0, 1.0, 1.0]; + dataSplitting = mCommandsProto.DATA_SPLITTING; + allAnalogChannels = mAnalogConstants.allAnalogChannels; + for (String aChannel in allAnalogChannels) { + analogInputSources[aChannel] = AnalogInputSource(aChannel); + } + sin1Frequency = 0; + sin2Frequency = 0; + squareWaveFrequency['SQR1'] = 0.0; + squareWaveFrequency['SQR2'] = 0.0; + squareWaveFrequency['SQR3'] = 0.0; + squareWaveFrequency['SQR4'] = 0.0; + + if (isConnected()) { + await runInitSequence(true); + } + } + + Future runInitSequence(bool loadCalibrationData) async { + if (!isConnected()) { + print("Check hardware connections. Not connected"); + } + streaming = false; + for (String aChannel in mAnalogConstants.biPolars) { + aChannels.add(AnalogAcquisitionChannel(aChannel)); + } + gainValues = mAnalogConstants.gains; + buffer = List.filled(10000, 0); + SOCKET_CAPACITANCE = 5e-11; + resistanceScaling = 1; + allDigitalChannels = DigitalChannel.digitalChannelNames; + gains['CH1'] = 0; + gains['CH2'] = 0; + for (int i = 0; i < 4; i++) { + dChannels.add(DigitalChannel(i)); + } + if (isConnected()) { + for (String temp in ['CH1', 'CH2']) { + await setGain(temp, 0, true); + } + for (String temp in ['SI1', 'SI1']) { + await loadEquation(temp, 'sine'); + } + } + calibrated = false; + } + + Future setGain(String channel, int gain, bool? force) async { + force ??= false; + if (gain < 0 || gain > 8) { + print("Invalid gain parameter. 0-7 only."); + return 0; + } + if (analogInputSources[channel]!.gainPGA == -1) { + print("No amplifier exists on this channel: $channel"); + return 0; + } + bool refresh = false; + if (gains[channel] != gain) { + gains[channel] = gain; + refresh = true; + } + if (refresh || force) { + analogInputSources[channel]!.setGain(gain); + if (gain > 7) { + gain = 0; + } + try { + mPacketHandler.sendByte(mCommandsProto.ADC); + mPacketHandler.sendByte(mCommandsProto.SET_PGA_GAIN); + mPacketHandler.sendByte(analogInputSources[channel]!.gainPGA); + mPacketHandler.sendByte(gain); + await mPacketHandler.getAcknowledgement(); + return gainValues[gain]; + } catch (e) { + print(e); + } + } + return 0; + } + + Future loadEquation(String channel, String function) async { + List span = List.filled(2, 0); + if (function == 'sine') { + span[0] = 0; + span[1] = 2 * pi; + waveType[channel] = 'sine'; + } else if (function == 'tria') { + span[0] = 0; + span[1] = 4; + waveType[channel] = 'tria'; + } else { + waveType[channel] = 'orbit'; + } + double factor = (span[1] - span[0]) / 512; + List x = []; + List y = []; + for (int i = 0; i < 512; i++) { + x.add(span[0] + i * factor); + switch (function) { + case 'sine': + y.add(sin(x[i])); + break; + case 'tria': + y.add((x[i] % 4 - 2).abs()); + break; + default: + break; + } + } + await _loadTable(channel, y, waveType[channel]!, -1); + } + + Future _loadTable( + String channel, List y, String mode, double amp) async { + waveType[channel] = mode; + List channels = []; + List points = y; + channels.add('SI1'); + channels.add('SI2'); + int num; + if (channels.contains(channel)) { + num = channels.indexOf(channel) + 1; + } else { + print("Channel doesn't exist. Try SI1 or SI2"); + return; + } + if (amp == -1) { + amp = 0.95; + } + double LARGE_MAX = 511 * amp, SMALL_MAX = 63 * amp; + double minimum = y.reduce(min); + for (int i = 0; i < y.length; i++) { + y[i] = y[i] - minimum; + } + double maximum = y.reduce(max); + List yMod1 = []; + for (int i = 0; i < y.length; i++) { + double temp = 1 - (y[i] / maximum); + yMod1.add((LARGE_MAX - LARGE_MAX * temp).round()); + } + y = []; + for (int i = 0; i < points.length; i += 16) { + y.add(points[i]); + } + minimum = y.reduce(min); + for (int i = 0; i < y.length; i++) { + y[i] = y[i] - minimum; + } + maximum = y.reduce(max); + List yMod2 = []; + for (int i = 0; i < y.length; i++) { + double temp = 1 - (y[i] / maximum); + yMod2.add((SMALL_MAX - SMALL_MAX * temp).round()); + } + + try { + mPacketHandler.sendByte(mCommandsProto.WAVEGEN); + switch (num) { + case 1: + mPacketHandler.sendByte(mCommandsProto.LOAD_WAVEFORM1); + break; + case 2: + mPacketHandler.sendByte(mCommandsProto.LOAD_WAVEFORM2); + break; + } + for (int a in yMod1) { + mPacketHandler.sendInt(a); + } + for (int a in yMod2) { + mPacketHandler.sendByte(a); + } + await mPacketHandler.getAcknowledgement(); + } catch (e) { + print(e); + } + } +} diff --git a/lib/constants.dart b/lib/constants.dart index f65495e30..0c4eefb61 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -56,3 +56,32 @@ List instrumentIcons = [ 'assets/icons/tile_icon_gas.png', 'assets/icons/tile_icon_gas.png', ]; + +List yAxisRanges = [ + '+/-16V', + '+/-8V', + '+/-4V', + '+/-3V', + '+/-2V', + '+/-1.5V', + '+/-1V', + '+/-500mV', + '+/-160V', +]; + +List rangeMenuEntries = [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', + 'CAP' + 'RES', + 'VOL', +]; + +List channelEntries = [ + 'CH1', + 'CH2', + 'CH3', + 'MIC', +]; diff --git a/lib/main.dart b/lib/main.dart index 7417741ab..48209138e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,27 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/providers/board_state_provider.dart'; +import 'package:pslab/providers/locator.dart'; import 'package:pslab/view/connect_device_screen.dart'; import 'package:pslab/view/faq_screen.dart'; import 'package:pslab/view/instruments_screen.dart'; import 'package:pslab/view/oscilloscope_screen.dart'; void main() { + setupLocator(); WidgetsFlutterBinding.ensureInitialized(); - runApp(const MyApp()); + getIt().initialize(); + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => getIt(), + ), + ], + child: const MyApp(), + ), + ); } class MyApp extends StatelessWidget { diff --git a/lib/others/science_lab_common.dart b/lib/others/science_lab_common.dart new file mode 100644 index 000000000..9aa21007f --- /dev/null +++ b/lib/others/science_lab_common.dart @@ -0,0 +1,25 @@ +import 'package:pslab/communication/communication_handler.dart'; +import 'package:pslab/communication/science_lab.dart'; + +class ScienceLabCommon { + static ScienceLab? scienceLab; + bool connected = false; + + ScienceLabCommon._privateConstructor(); + + static final ScienceLabCommon _instance = + ScienceLabCommon._privateConstructor(); + + factory ScienceLabCommon() => _instance; + + Future openDevice(CommunicationHandler communicationHandler) async { + scienceLab = ScienceLab(communicationHandler); + await scienceLab!.connect(); + if (!scienceLab!.isConnected()) { + print("Error in connection"); + return false; + } + connected = true; + return true; + } +} diff --git a/lib/providers/board_state_provider.dart b/lib/providers/board_state_provider.dart new file mode 100644 index 000000000..0f1eae790 --- /dev/null +++ b/lib/providers/board_state_provider.dart @@ -0,0 +1,55 @@ +import 'package:flutter/cupertino.dart'; +import 'package:pslab/communication/communication_handler.dart'; +import 'package:usb_serial/usb_serial.dart'; + +import '../others/science_lab_common.dart'; + +class BoardStateProvider extends ChangeNotifier { + bool initialisationStatus = false; + bool pslabIsConnected = false; + bool hasPermission = false; + late CommunicationHandler communicationHandler; + late ScienceLabCommon scienceLabCommon; + String pslabVersionID = 'Not Connected'; + + Future initialize() async { + scienceLabCommon = ScienceLabCommon(); + communicationHandler = CommunicationHandler(); + await communicationHandler.initialize(); + pslabIsConnected = await scienceLabCommon.openDevice(communicationHandler); + setPSLabVersionIDs(); + UsbSerial.usbEventStream?.listen((UsbEvent usbEvent) async { + if (usbEvent.event == UsbEvent.ACTION_USB_ATTACHED) { + if (await attemptToConnectPSLab()) { + pslabIsConnected = + await scienceLabCommon.openDevice(communicationHandler); + setPSLabVersionIDs(); + } + } else if (usbEvent.event == UsbEvent.ACTION_USB_DETACHED) { + communicationHandler.connected = false; + pslabIsConnected = false; + pslabVersionID = 'Not Connected'; + notifyListeners(); + } + }); + } + + Future setPSLabVersionIDs() async { + pslabVersionID = await ScienceLabCommon.scienceLab!.getVersion(); + notifyListeners(); + } + + Future attemptToConnectPSLab() async { + scienceLabCommon = ScienceLabCommon(); + if (communicationHandler.isConnected()) { + print("Device Connected Successfully"); + } else { + communicationHandler = CommunicationHandler(); + await communicationHandler.initialize(); + if (communicationHandler.isDeviceFound()) { + return true; + } + } + return false; + } +} diff --git a/lib/providers/locator.dart b/lib/providers/locator.dart new file mode 100644 index 000000000..33df923ff --- /dev/null +++ b/lib/providers/locator.dart @@ -0,0 +1,8 @@ +import 'package:get_it/get_it.dart'; +import 'package:pslab/providers/board_state_provider.dart'; + +final GetIt getIt = GetIt.instance; + +void setupLocator() { + getIt.registerLazySingleton(() => BoardStateProvider()); +} diff --git a/lib/providers/oscilloscope_state_provider.dart b/lib/providers/oscilloscope_state_provider.dart index 3172ecee7..e92ff5d0c 100644 --- a/lib/providers/oscilloscope_state_provider.dart +++ b/lib/providers/oscilloscope_state_provider.dart @@ -3,6 +3,15 @@ import 'package:flutter/material.dart'; class OscilloscopeStateProvider extends ChangeNotifier { int _selectedIndex = 0; + bool? isCH1Selected = false; + bool? isCH2Selected = false; + bool? isCH3Selected = false; + bool? isMICSelected = false; + bool? isInBuiltMICSelected = false; + bool? isAudioInputSelected = false; + + double yAxisRange = 16; + int get selectedIndex => _selectedIndex; void updateSelectedIndex(int index) { diff --git a/lib/view/widgets/channel_parameters_widget.dart b/lib/view/widgets/channel_parameters_widget.dart index 1f3517075..5a1b3ea7a 100644 --- a/lib/view/widgets/channel_parameters_widget.dart +++ b/lib/view/widgets/channel_parameters_widget.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/providers/oscilloscope_state_provider.dart'; + +import '../../constants.dart'; class ChannelParametersWidget extends StatefulWidget { const ChannelParametersWidget({super.key}); @@ -9,14 +13,10 @@ class ChannelParametersWidget extends StatefulWidget { } class _ChannelParametersState extends State { - bool? isCH1Selected = false; - bool? isCH2Selected = false; - bool? isCH3Selected = false; - bool? isMICSelected = false; - bool? isInBuiltMICSelected = false; - @override Widget build(BuildContext context) { + OscilloscopeStateProvider oscilloscopeStateProvider = + Provider.of(context); return Stack( children: [ Container( @@ -36,12 +36,12 @@ class _ChannelParametersState extends State { children: [ Checkbox( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: isCH1Selected, + value: oscilloscopeStateProvider.isCH1Selected, activeColor: const Color(0xFFCE525F), onChanged: (bool? value) { setState( () { - isCH1Selected = value; + oscilloscopeStateProvider.isCH1Selected = value; }, ); }, @@ -72,19 +72,9 @@ class _ChannelParametersState extends State { Padding( padding: EdgeInsets.only(top: 1.h, left: 4.w), child: DropdownMenu( - initialSelection: '+/-16V', + initialSelection: yAxisRanges[0], width: 60.w, - dropdownMenuEntries: [ - '+/-16V', - '+/-8V', - '+/-4V', - '+/-3V', - '+/-2V', - '+/-1.5V', - '+/-1V', - '+/-500mV', - '+/-160V', - ].map( + dropdownMenuEntries: yAxisRanges.map( (String value) { return DropdownMenuEntry( label: value, @@ -98,22 +88,48 @@ class _ChannelParametersState extends State { textStyle: const TextStyle( fontSize: 14, ), + onSelected: (String? value) { + switch (yAxisRanges.indexOf(value!)) { + case 0: + oscilloscopeStateProvider.yAxisRange = 16; + break; + case 1: + oscilloscopeStateProvider.yAxisRange = 8; + break; + case 2: + oscilloscopeStateProvider.yAxisRange = 4; + break; + case 3: + oscilloscopeStateProvider.yAxisRange = 3; + break; + case 4: + oscilloscopeStateProvider.yAxisRange = 2; + break; + case 5: + oscilloscopeStateProvider.yAxisRange = 1.5; + break; + case 6: + oscilloscopeStateProvider.yAxisRange = 1; + break; + case 7: + oscilloscopeStateProvider.yAxisRange = 500; + break; + case 8: + oscilloscopeStateProvider.yAxisRange = 160; + break; + default: + oscilloscopeStateProvider.yAxisRange = 16; + break; + } + }, ), ), Padding( padding: EdgeInsets.only(top: 0.h), child: DropdownMenu( width: 50.w, - initialSelection: 'CH1', - dropdownMenuEntries: [ - 'CH1', - 'CH2', - 'CH3', - 'MIC', - 'RES', - 'VOL', - 'CAP', - ].map( + initialSelection: rangeMenuEntries[0], + dropdownMenuEntries: rangeMenuEntries.map( (String value) { return DropdownMenuEntry( label: value, @@ -141,12 +157,12 @@ class _ChannelParametersState extends State { children: [ Checkbox( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: isCH2Selected, + value: oscilloscopeStateProvider.isCH2Selected, activeColor: const Color(0xFFCE525F), onChanged: (bool? value) { setState( () { - isCH2Selected = value; + oscilloscopeStateProvider.isCH2Selected = value; }, ); }, @@ -214,12 +230,12 @@ class _ChannelParametersState extends State { children: [ Checkbox( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: isCH3Selected, + value: oscilloscopeStateProvider.isCH3Selected, activeColor: const Color(0xFFCE525F), onChanged: (bool? value) { setState( () { - isCH3Selected = value; + oscilloscopeStateProvider.isCH3Selected = value; }, ); }, @@ -249,16 +265,23 @@ class _ChannelParametersState extends State { activeColor: const Color(0xFFCE525F), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: true, - groupValue: isInBuiltMICSelected, + groupValue: + oscilloscopeStateProvider.isInBuiltMICSelected, toggleable: true, onChanged: (bool? value) { setState( () { if (value == null) { - isInBuiltMICSelected = false; + oscilloscopeStateProvider.isInBuiltMICSelected = + false; + oscilloscopeStateProvider.isAudioInputSelected = + false; } else { - isInBuiltMICSelected = value; - isMICSelected = !value; + oscilloscopeStateProvider.isAudioInputSelected = + true; + oscilloscopeStateProvider.isInBuiltMICSelected = + value; + oscilloscopeStateProvider.isMICSelected = !value; } }, ); @@ -279,16 +302,21 @@ class _ChannelParametersState extends State { activeColor: const Color(0xFFCE525F), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: true, - groupValue: isMICSelected, + groupValue: oscilloscopeStateProvider.isMICSelected, toggleable: true, onChanged: (bool? value) { setState( () { if (value == null) { - isMICSelected = false; + oscilloscopeStateProvider.isMICSelected = false; + oscilloscopeStateProvider.isAudioInputSelected = + false; } else { - isMICSelected = value; - isInBuiltMICSelected = !value; + oscilloscopeStateProvider.isAudioInputSelected = + true; + oscilloscopeStateProvider.isMICSelected = value; + oscilloscopeStateProvider.isInBuiltMICSelected = + !value; } }, ); diff --git a/lib/view/widgets/main_scaffold_widget.dart b/lib/view/widgets/main_scaffold_widget.dart index 9e50738dc..535c6f4b0 100644 --- a/lib/view/widgets/main_scaffold_widget.dart +++ b/lib/view/widgets/main_scaffold_widget.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/providers/board_state_provider.dart'; import 'navigation_drawer.dart'; @@ -10,6 +12,7 @@ class MainScaffold extends StatefulWidget { final int index; final List? actions; final String icUsbDisconnected = 'assets/icons/ic_usb_disconnected.png'; + final String icUsbConnected = 'assets/icons/ic_usb_connected.png'; const MainScaffold( {super.key, @@ -53,14 +56,20 @@ class _MainScaffoldState extends State { ), ), actions: [ - IconButton( - icon: Image.asset( - widget.icUsbDisconnected, - width: 24, - height: 24, - ), - onPressed: () { - /**/ + Consumer( + builder: (context, provider, _) { + return IconButton( + icon: Image.asset( + provider.pslabIsConnected + ? widget.icUsbConnected + : widget.icUsbDisconnected, + width: 24, + height: 24, + ), + onPressed: () { + /**/ + }, + ); }, ), IconButton( diff --git a/lib/view/widgets/navigation_drawer.dart b/lib/view/widgets/navigation_drawer.dart index 01b867b22..dc1051689 100644 --- a/lib/view/widgets/navigation_drawer.dart +++ b/lib/view/widgets/navigation_drawer.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:pslab/providers/board_state_provider.dart'; class NavDrawer extends StatefulWidget { final int selectedIndex; @@ -38,11 +40,17 @@ class _NavDrawerState extends State { fit: BoxFit.contain, ), ), - const Padding( - padding: EdgeInsets.only(top: 16), - child: Text('Not Connected', - style: TextStyle( - fontSize: 14, fontStyle: FontStyle.normal)), + Consumer( + builder: (context, provider, _) { + return Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + provider.pslabVersionID, + style: const TextStyle( + fontSize: 14, fontStyle: FontStyle.normal), + ), + ); + }, ), ], ), diff --git a/pubspec.lock b/pubspec.lock index 7bc7df166..aa5f3f66e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" crypto: dependency: transitive description: @@ -131,6 +131,14 @@ packages: description: flutter source: sdk version: "0.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103 + url: "https://pub.dev" + source: hosted + version: "8.0.3" google_fonts: dependency: "direct main" description: @@ -159,18 +167,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -307,6 +315,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + polynomial: + dependency: "direct main" + description: + name: polynomial + sha256: "2a2e58e70a131a02d6073ff11d56dde8392d76d075ffdd038c067fc3e8258733" + url: "https://pub.dev" + source: hosted + version: "1.0.0" provider: dependency: "direct main" description: @@ -319,7 +335,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -332,10 +348,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -348,10 +364,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -364,10 +380,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" typed_data: dependency: transitive description: @@ -376,6 +392,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + usb_serial: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: "35f3cca1454f03f07bb646bfb36aed32011e5443" + url: "https://github.com/AsCress/usbserial.git" + source: git + version: "0.5.1" vector_graphics: dependency: transitive description: @@ -412,10 +437,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" web: dependency: transitive description: @@ -441,5 +466,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.5.4 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 25ac38acb..f0ae39912 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,12 @@ dependencies: google_fonts: ^6.2.1 fl_chart: ^0.70.1 provider: ^6.1.2 + usb_serial: + git: + url: https://github.com/AsCress/usbserial.git + ref: master + polynomial: 1.0.0 + get_it: ^8.0.3 dev_dependencies: flutter_test: From 9ef10186604c9a38402e8238a58bc6dab8d372c0 Mon Sep 17 00:00:00 2001 From: Anashuman Singh Date: Sun, 26 Jan 2025 22:48:38 +0530 Subject: [PATCH 4/4] chore: add support for dynamic, scalable layouts --- android/app/src/main/AndroidManifest.xml | 1 + ios/Podfile | 3 +- ios/Runner/Info.plist | 2 + .../analogChannel/analog_input_source.dart | 20 +- lib/communication/analytics_class.dart | 328 ++++++++++++++++++ lib/communication/communication_handler.dart | 1 + lib/constants.dart | 4 +- lib/main.dart | 41 ++- lib/others/audio_jack.dart | 32 ++ lib/view/instruments_screen.dart | 13 +- lib/view/oscilloscope_screen.dart | 69 ++-- .../widgets/channel_parameters_widget.dart | 189 ++++------ lib/view/widgets/data_analysis_widget.dart | 204 ++++++----- lib/view/widgets/navigation_drawer.dart | 16 +- .../widgets/oscilloscope_screen_tabs.dart | 8 +- lib/view/widgets/timebase_trigger_widget.dart | 73 ++-- lib/view/widgets/xyplot_widget.dart | 76 ++-- pubspec.lock | 53 +-- pubspec.yaml | 7 +- 19 files changed, 741 insertions(+), 399 deletions(-) create mode 100644 lib/communication/analytics_class.dart create mode 100644 lib/others/audio_jack.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b2077ed1f..be651e46f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + LaunchScreen UIMainStoryboardFile Main + NSMicrophoneUsageDescription + App needs Microphone access to capture audio UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/lib/communication/analogChannel/analog_input_source.dart b/lib/communication/analogChannel/analog_input_source.dart index f56c9f3e7..8a4359383 100644 --- a/lib/communication/analogChannel/analog_input_source.dart +++ b/lib/communication/analogChannel/analog_input_source.dart @@ -1,4 +1,6 @@ -import 'package:polynomial/polynomial.dart'; +import 'dart:core'; + +import 'package:data/data.dart'; import 'package:pslab/communication/analogChannel/analog_constants.dart'; class AnalogInputSource { @@ -20,8 +22,8 @@ class AnalogInputSource { _gainValues = analogConstants.gains; CHOSA = analogConstants.picADCMultiplex[_channelName]!; - calPoly10 = Polynomial([0, 3.3 / 1023, 0]); - calPoly12 = Polynomial([0, 3.3 / 4095, 0]); + calPoly10 = Polynomial.fromCoefficients(DataType.float, [0, 3.3 / 1023, 0]); + calPoly12 = Polynomial.fromCoefficients(DataType.float, [0, 3.3 / 4095, 0]); if (_range[1] - _range[0] < 0) { inverted = true; @@ -65,12 +67,16 @@ class AnalogInputSource { slope = 2 * (B - A); intercept = 2 * A; if (!calibrationReady || _gain == 8) { - calPoly10 = Polynomial([intercept, slope / 1023, 0]); - calPoly12 = Polynomial([intercept, slope / 4095, 0]); + calPoly10 = Polynomial.fromCoefficients( + DataType.float, [intercept, slope / 1023, 0]); + calPoly12 = Polynomial.fromCoefficients( + DataType.float, [intercept, slope / 4095, 0]); } - voltToCode10 = Polynomial([-1023 * intercept / slope, 1023 / slope, 0]); - voltToCode12 = Polynomial([-4095 * intercept / slope, 4095, 0]); + voltToCode10 = Polynomial.fromCoefficients( + DataType.float, [-1023 * intercept / slope, 1023 / slope, 0]); + voltToCode12 = Polynomial.fromCoefficients( + DataType.float, [-4095 * intercept / slope, 4095.0, 0]); } List cal12(List raw) { diff --git a/lib/communication/analytics_class.dart b/lib/communication/analytics_class.dart new file mode 100644 index 000000000..ea99777b2 --- /dev/null +++ b/lib/communication/analytics_class.dart @@ -0,0 +1,328 @@ +import 'dart:math'; + +import 'package:data/data.dart'; + +class AnalyticsClass { + //---------------------------- Sine Fit ---------------------------------// + List sineFit(List xReal, List yReal) { + int n = xReal.length; + int index = 0; + List frequencyArray = []; + List yReal2 = List.filled(yReal.length, 0); + List yHat = []; + List yHatSquare = []; + double maximum = 0; + double returnOffset = 0; + double frequency; + double returnFrequency; + double amplitude; + double returnAmplitude; + double phase; + double returnPhase; + List initialValues; + List complex; + + double offset = (yReal.reduce(max) + yReal.reduce(min)) / 2; + for (int i = 0; i < yReal.length; i++) { + yReal2[i] = yReal[i] - offset; + } + + complex = fft(yReal2.map((x) => Complex(x)).toList()); + yHat = fftToRfft(complex); + for (int i = 0; i < yHat.length; i++) { + yHatSquare[i] = pow(yHat[i], 2).toDouble(); + if (yHatSquare[i] > maximum) { + maximum = yHatSquare[i]; + index = i; + } + } + + frequencyArray = rfftFrequency(n, (xReal[1] - xReal[0]) / (2 * pi)); + frequency = frequencyArray[index]; + frequency /= (2 * pi); + + amplitude = (yReal.reduce(max) - yReal.reduce(min)) / 2; + + phase = 0; + + initialValues = [amplitude, frequency, phase, 0]; + + LevenbergMarquardt optimizer = LevenbergMarquardt( + ParametrizedUnaryFunction.list( + DataType.float, + 4, + (List params) { + double a1 = params[0]; + double a2 = params[1]; + double a3 = params[2]; + double a4 = params[3]; + return (x) => (a4 + a1 * sin((a2 * (2 * pi)).abs() * x + a3)); + }, + ), + initialValues: initialValues, + ); + + LevenbergMarquardtResult result = optimizer.fit( + xs: Vector.fromList(DataType.float, xReal), + ys: Vector.fromList(DataType.float, yReal2), + ); + + amplitude = result.parameters[0]; + frequency = result.parameters[1]; + phase = result.parameters[2]; + returnOffset = result.parameters[3]; + + if (frequency < 0) { + print("sineFit: Negative frequency"); + } + + returnOffset += offset; + returnPhase = ((phase) * 180 / (3.14)); + if (amplitude < 0) { + returnPhase -= 180; + } + if (returnPhase < 0) { + returnPhase = (returnPhase + 720) % 360; + } + returnFrequency = 1e6 * frequency.abs(); + returnAmplitude = amplitude.abs(); + return [returnAmplitude, returnFrequency, returnOffset, returnPhase]; + } + +//---------------------------- Square Fit ---------------------------------// + List squareFit(List xReal, List yReal) { + double mx = yReal.reduce(max); + double mn = xReal.reduce(min); + double offset = (mx + mn) / 2; + double sumGreaterThanOffset = 0; + double sumLesserThanOffset = 0; + double n1 = 0; + double n2 = 0; + List yTmp = List.filled(yReal.length, 0); + List yReal2 = List.filled(yReal.length, 0); + List initialValues; + double returnOffset; + double returnFrequency; + double returnAmplitude; + double returnPhase; + double returnDC; + + for (int i = 0; i < yReal.length; i++) { + yReal2[i] = yReal[i] - offset; + } + + for (int i = 0; i < yReal.length; i++) { + if (yReal[i] > offset) { + sumGreaterThanOffset += yReal[i]; + yTmp[i] = 2; + n1++; + } else if (yReal[i] < offset) { + sumLesserThanOffset += yReal[i]; + yTmp[i] = 0; + n2++; + } + } + + double amplitude = (sumGreaterThanOffset / n1) - (sumLesserThanOffset / n2); + List bools = []; + double tmp; + for (int i = 0; i < yTmp.length - 1; i++) { + tmp = yTmp[i + 1] - yTmp[i]; + tmp = tmp.abs(); + bools[i] = tmp > 1; + } + List edges = List.filled(bools.length, 0); + List levels = List.filled(bools.length, 0); + int j = 0; + for (int i = 0; i < bools.length; i++) { + if (bools[i]) { + edges[j] = xReal[i]; + levels[j] = yTmp[i]; + j++; + } + } + + double frequency = 1 / (edges[2] - edges[0]); + double phase = edges[0]; + double dc = 0.5; + + if (edges.length >= 4) { + if (levels[0] == 0) { + dc = (edges[1] - edges[0]) / (edges[2] - edges[0]); + } else { + dc = (edges[2] - edges[1]) / (edges[3] - edges[1]); + phase = edges[1]; + } + } + + initialValues = [amplitude, frequency, phase, dc, 0]; + + LevenbergMarquardt optimizer = LevenbergMarquardt( + ParametrizedUnaryFunction.list( + DataType.float, + 5, + (List params) { + double amp = params[0]; + double freq = params[1]; + double phase = params[2]; + double dc = params[3]; + double offset = params[4]; + return (x) => (offset + + amp * signalSquare(2 * pi * freq * (x - phase), freq, dc)); + }, + ), + initialValues: initialValues, + ); + + LevenbergMarquardtResult result = optimizer.fit( + xs: Vector.fromList(DataType.float, xReal), + ys: Vector.fromList(DataType.float, yReal2), + ); + + amplitude = result.parameters[0]; + frequency = result.parameters[1]; + phase = result.parameters[2]; + dc = result.parameters[3]; + returnOffset = result.parameters[4]; + + if (frequency < 0) { + print("squareFit: Negative frequency"); + } + + returnOffset += offset; + returnFrequency = 1e6 * frequency.abs(); + returnAmplitude = amplitude.abs(); + returnPhase = phase; + returnDC = dc; + + return [ + returnAmplitude, + returnFrequency, + returnPhase, + returnDC, + returnOffset + ]; + } + + double findSignalFrequency(List voltage, double samplingInterval) { + int voltageLength = voltage.length; + List frequency; + List amplitude; + int index = 0; + double max = 0; + List complex; + + double voltageMean = voltage.arithmeticMean(); + for (int i = 0; i < voltageLength; i++) { + voltage[i] = voltage[i] - voltageMean; + } + frequency = fftFrequency(voltageLength, samplingInterval) + .sublist(0, (voltageLength / 2) as int); + complex = fft(voltage.map((x) => Complex(x)).toList()); + amplitude = List.filled(complex.length / 2 as int, 0); + for (int i = 0; i < complex.length / 2; i++) { + // take only the +ive half of the fft result + amplitude[i] = complex[i].abs() / voltageLength; + if (amplitude[i] > max) { + // search for the tallest peak, the fundamental + max = amplitude[i]; + index = i; + } + } + double noiseThreshold = 0.1; + if (max >= noiseThreshold) { + return frequency[index]; + } else { + return -1; + } + } + + double findFrequency(List voltage, double samplingInterval) { + int voltageLength = voltage.length; + List frequency; + List amplitude; + int index = 0; + double max = 0; + List complex; + + double voltageMean = voltage.arithmeticMean(); + for (int i = 0; i < voltageLength; i++) { + voltage[i] = voltage[i] - voltageMean; + } + frequency = fftFrequency(voltageLength, samplingInterval) + .sublist(0, (voltageLength / 2) as int); + complex = fft(voltage.map((x) => Complex(x)).toList()); + amplitude = List.filled(complex.length / 2 as int, 0); + for (int i = 0; i < complex.length / 2; i++) { + // take only the +ive half of the fft result + amplitude[i] = complex[i].abs() / voltageLength; + if (amplitude[i] > max) { + // search for the tallest peak, the fundamental + max = amplitude[i]; + index = i; + } + } + return frequency[index]; + } + + List rfftFrequency(int n, double space) { + List returnArray = List.filled(n + 1, 0); + for (int i = 0; i < n + 1; i++) { + returnArray[i] = (i / 2).floor() / (n * space); + } + return returnArray.sublist(1, returnArray.length); + } + + List fftFrequency(int n, double space) { + double value = 1.0 / (n * space); + int N = ((n - 1) / 2).floor() + 1; + List results = List.filled(n, 0); + for (int i = 0; i < N; i++) { + results[i] = i.toDouble(); + results[i] = results[i] * value; + } + int j = N; + for (int i = -(((n - 1) / 2).floor()); i < 0; i++) { + results[j] = i.toDouble(); + results[j] = results[j] * value; + j++; + } + return results; + } + + List fftToRfft(List complex) { + List real = List.filled(complex.length, 0); + List imaginary = List.filled(complex.length, 0); + List result = List.filled(complex.length, 0); + int j = 0; + int k = 0; + int l = 0; + for (int i = 0; i < complex.length / 2 + 1; i++) { + real[i] = complex[i].real.toDouble(); + imaginary[i] = complex[i].imaginary.toDouble(); + } + + for (int i = 0; i < complex.length / 2 + 1; i++) { + if (real[j] == 0.0 && imaginary[k] == 0) { + result[l++] = 0.0; + j++; + k++; + } else if (real[j] != 0 && imaginary[k] == 0) { + result[l++] = real[j++]; + k++; + } else { + result[l++] = real[j++]; + result[l++] = imaginary[k++]; + } + } + return result; + } + + double signalSquare(double xAxisValue, double freq, double dc) { + if (xAxisValue % (2 * pi * freq) <= dc) { + return 1; + } else { + return -1; + } + } +} diff --git a/lib/communication/communication_handler.dart b/lib/communication/communication_handler.dart index 90b5283ab..0fc219d44 100644 --- a/lib/communication/communication_handler.dart +++ b/lib/communication/communication_handler.dart @@ -74,6 +74,7 @@ class CommunicationHandler { } return numBytesRead; } + print("Read: $numBytesRead"); return numBytesRead; } diff --git a/lib/constants.dart b/lib/constants.dart index 0c4eefb61..c43fe0b27 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -74,8 +74,8 @@ List rangeMenuEntries = [ 'CH2', 'CH3', 'MIC', - 'CAP' - 'RES', + 'CAP', + 'RES', 'VOL', ]; diff --git a/lib/main.dart b/lib/main.dart index 48209138e..02db3a084 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; import 'package:pslab/providers/board_state_provider.dart'; import 'package:pslab/providers/locator.dart'; @@ -8,6 +7,8 @@ import 'package:pslab/view/faq_screen.dart'; import 'package:pslab/view/instruments_screen.dart'; import 'package:pslab/view/oscilloscope_screen.dart'; +import 'constants.dart'; + void main() { setupLocator(); WidgetsFlutterBinding.ensureInitialized(); @@ -29,24 +30,28 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return ScreenUtilInit( - designSize: const Size(360, 690), - builder: (context, child) { - return MaterialApp( - debugShowCheckedModeBanner: false, - theme: ThemeData( - colorSchemeSeed: Colors.white, - useMaterial3: true, - ), - initialRoute: '/', - routes: { - '/': (context) => const InstrumentsScreen(), - '/oscilloscope': (context) => const OscilloscopeScreen(), - '/connectDevice': (context) => const ConnectDeviceScreen(), - '/faq': (context) => const FAQScreen(), - }, - ); + _preCacheImages(context); + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorSchemeSeed: Colors.white, + useMaterial3: true, + ), + initialRoute: '/', + routes: { + '/': (context) => const InstrumentsScreen(), + '/oscilloscope': (context) => const OscilloscopeScreen(), + '/connectDevice': (context) => const ConnectDeviceScreen(), + '/faq': (context) => const FAQScreen(), }, ); } } + +void _preCacheImages(BuildContext context) { + for (final path in instrumentIcons) { + precacheImage(AssetImage(path), context); + } + precacheImage( + const AssetImage('assets/icons/ic_nav_header_logo.png'), context); +} diff --git a/lib/others/audio_jack.dart b/lib/others/audio_jack.dart new file mode 100644 index 000000000..503063b1e --- /dev/null +++ b/lib/others/audio_jack.dart @@ -0,0 +1,32 @@ +import 'package:flutter_audio_capture/flutter_audio_capture.dart'; + +class AudioJack { + static const int samplingRate = 44100; + final FlutterAudioCapture flutterAudioCapture = FlutterAudioCapture(); + + late List _audioBuffer; + + AudioJack(); + + Future _configure() async { + await flutterAudioCapture.start(_listener, _onError, + sampleRate: samplingRate); + return true; + } + + List read() { + return _audioBuffer; + } + + void _listener(dynamic obj) { + _audioBuffer = obj.cast(); + } + + void _onError(Object e) { + print(e); + } + + Future close() async { + await flutterAudioCapture.stop(); + } +} diff --git a/lib/view/instruments_screen.dart b/lib/view/instruments_screen.dart index bead05324..ce837b47e 100644 --- a/lib/view/instruments_screen.dart +++ b/lib/view/instruments_screen.dart @@ -15,7 +15,16 @@ class _InstrumentsScreenState extends State { void _onItemTapped(int index) { switch (index) { case 0: - Navigator.pushNamed(context, '/oscilloscope'); + if (Navigator.canPop(context) && + ModalRoute.of(context)?.settings.name == '/oscilloscope') { + Navigator.popUntil(context, ModalRoute.withName('/oscilloscope')); + } else { + Navigator.pushNamedAndRemoveUntil( + context, + '/oscilloscope', + (route) => route.isFirst, + ); + } break; default: break; @@ -35,8 +44,6 @@ class _InstrumentsScreenState extends State { SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, ]); } diff --git a/lib/view/oscilloscope_screen.dart b/lib/view/oscilloscope_screen.dart index 455237836..a1673aa0f 100644 --- a/lib/view/oscilloscope_screen.dart +++ b/lib/view/oscilloscope_screen.dart @@ -1,7 +1,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; import 'package:pslab/view/widgets/channel_parameters_widget.dart'; import 'package:pslab/view/widgets/common_scaffold_widget.dart'; @@ -49,43 +48,49 @@ class _OscilloscopeScreenState extends State { margin: const EdgeInsets.only(left: 5, top: 5), child: Row( children: [ - Container( - width: 310.w, - margin: const EdgeInsets.only(right: 5), - child: Column( - children: [ - SizedBox( - height: 380.h, - child: LineChart( - LineChartData( - backgroundColor: Colors.black, - titlesData: const FlTitlesData(show: false), - borderData: FlBorderData(show: false), + Expanded( + flex: 87, + child: Container( + margin: const EdgeInsets.only(right: 5), + child: Column( + children: [ + Expanded( + flex: 66, + child: SizedBox( + child: LineChart( + LineChartData( + backgroundColor: Colors.black, + titlesData: const FlTitlesData(show: false), + borderData: FlBorderData(show: false), + ), + ), ), ), - ), - Expanded( - child: Consumer( - builder: (context, provider, _) { - switch (provider.selectedIndex) { - case 0: - return const ChannelParametersWidget(); - case 1: - return const TimebaseTriggerWidget(); - case 2: - return const DataAnalysisWidget(); // Replace with your widget for Tab 3 - case 3: - return const XYPlotWidget(); - default: - return const ChannelParametersWidget(); - } - }, + Expanded( + flex: 34, + child: Consumer( + builder: (context, provider, _) { + switch (provider.selectedIndex) { + case 0: + return const ChannelParametersWidget(); + case 1: + return const TimebaseTriggerWidget(); + case 2: + return const DataAnalysisWidget(); // Replace with your widget for Tab 3 + case 3: + return const XYPlotWidget(); + default: + return const ChannelParametersWidget(); + } + }, + ), ), - ), - ], + ], + ), ), ), const Expanded( + flex: 13, child: OscilloscopeScreenTabs(), ) ], diff --git a/lib/view/widgets/channel_parameters_widget.dart b/lib/view/widgets/channel_parameters_widget.dart index 5a1b3ea7a..8a7c665d5 100644 --- a/lib/view/widgets/channel_parameters_widget.dart +++ b/lib/view/widgets/channel_parameters_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; import 'package:pslab/providers/oscilloscope_state_provider.dart'; @@ -28,8 +27,8 @@ class _ChannelParametersState extends State { child: Stack( children: [ Positioned( - top: 0.h, - left: 2.w, + top: -4, + left: 4, child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, @@ -41,39 +40,36 @@ class _ChannelParametersState extends State { onChanged: (bool? value) { setState( () { - oscilloscopeStateProvider.isCH1Selected = value; + oscilloscopeStateProvider.isCH1Selected = value!; }, ); }, ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: const Text( - 'CH1', - style: TextStyle( - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, - fontSize: 15, - ), + const Text( + 'CH1', + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontSize: 15, ), ), - Padding( - padding: EdgeInsets.only(top: 1.h, left: 3.w), - child: const Text( + const Padding( + padding: EdgeInsets.only(left: 8), + child: Text( 'Range', style: TextStyle( color: Color(0xFF424242), fontWeight: FontWeight.normal, fontStyle: FontStyle.normal, - fontSize: 14, + fontSize: 15, ), ), ), Padding( - padding: EdgeInsets.only(top: 1.h, left: 4.w), + padding: const EdgeInsets.only(left: 8), child: DropdownMenu( initialSelection: yAxisRanges[0], - width: 60.w, + width: 120, dropdownMenuEntries: yAxisRanges.map( (String value) { return DropdownMenuEntry( @@ -85,9 +81,7 @@ class _ChannelParametersState extends State { inputDecorationTheme: const InputDecorationTheme( border: InputBorder.none, ), - textStyle: const TextStyle( - fontSize: 14, - ), + textStyle: const TextStyle(fontSize: 15), onSelected: (String? value) { switch (yAxisRanges.indexOf(value!)) { case 0: @@ -124,33 +118,30 @@ class _ChannelParametersState extends State { }, ), ), - Padding( - padding: EdgeInsets.only(top: 0.h), - child: DropdownMenu( - width: 50.w, - initialSelection: rangeMenuEntries[0], - dropdownMenuEntries: rangeMenuEntries.map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, - ); - }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, - ), - textStyle: const TextStyle( - fontSize: 15, - ), + DropdownMenu( + width: 90, + initialSelection: rangeMenuEntries[0], + dropdownMenuEntries: rangeMenuEntries.map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 15, ), ), ], ), ), Positioned( - left: 2.w, - bottom: 8.h, + left: 4, + bottom: 2, child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, @@ -162,54 +153,37 @@ class _ChannelParametersState extends State { onChanged: (bool? value) { setState( () { - oscilloscopeStateProvider.isCH2Selected = value; + oscilloscopeStateProvider.isCH2Selected = value!; }, ); }, ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: const Text( - 'CH2', - style: TextStyle( - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, - fontSize: 15, - ), + const Text( + 'CH2', + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontSize: 15, ), ), - Padding( - padding: EdgeInsets.only(top: 1.h, left: 3.w), - child: const Text( + const Padding( + padding: EdgeInsets.only(left: 8), + child: Text( 'Range', style: TextStyle( color: Color(0xFF424242), fontWeight: FontWeight.normal, fontStyle: FontStyle.normal, - fontSize: 14, + fontSize: 15, ), ), ), - Padding( - padding: EdgeInsets.only(top: 2.h, left: 4.w), + const Padding( + padding: EdgeInsets.only(left: 8), child: SizedBox( - width: 60.w, - child: const Text( + width: 120, + child: Text( '+/-16V', - style: TextStyle( - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 14, - ), - ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: SizedBox( - width: 45.w, - child: const Text( - 'CH2', style: TextStyle( fontStyle: FontStyle.normal, fontWeight: FontWeight.normal, @@ -222,8 +196,8 @@ class _ChannelParametersState extends State { ), ), Positioned( - top: 16.h, - right: 4.w, + top: 4, + right: 8, child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, @@ -235,28 +209,25 @@ class _ChannelParametersState extends State { onChanged: (bool? value) { setState( () { - oscilloscopeStateProvider.isCH3Selected = value; + oscilloscopeStateProvider.isCH3Selected = value!; }, ); }, ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: const Text( - 'CH3 (+/- 3.3V)', - style: TextStyle( - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, - fontSize: 15, - ), + const Text( + 'CH3 (+/- 3.3V)', + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontSize: 15, ), ), ], ), ), Positioned( - bottom: 8.h, - right: 4.w, + bottom: 2, + right: 8, child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, @@ -287,15 +258,12 @@ class _ChannelParametersState extends State { ); }, ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: const Text( - 'In-Built MIC', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, - ), + const Text( + 'In-Built MIC', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, ), ), Radio( @@ -322,15 +290,12 @@ class _ChannelParametersState extends State { ); }, ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: const Text( - 'PSLab MIC', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, - ), + const Text( + 'PSLab MIC', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, ), ), ], @@ -340,16 +305,16 @@ class _ChannelParametersState extends State { ), ), Positioned( - left: 0.w, - right: 0.w, - top: 2.h, + left: 0, + right: 0, + top: 1, child: Align( alignment: Alignment.center, child: Container( - padding: EdgeInsets.symmetric(horizontal: 2.w), + padding: const EdgeInsets.symmetric(horizontal: 2), decoration: const BoxDecoration(color: Colors.white), child: const Text( - 'Channel Parameters', + 'Channels', style: TextStyle( color: Color(0xFFC72C2C), fontStyle: FontStyle.normal, diff --git a/lib/view/widgets/data_analysis_widget.dart b/lib/view/widgets/data_analysis_widget.dart index 4231bcf3d..afed0a64c 100644 --- a/lib/view/widgets/data_analysis_widget.dart +++ b/lib/view/widgets/data_analysis_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; class DataAnalysisWidget extends StatefulWidget { const DataAnalysisWidget({super.key}); @@ -30,75 +29,45 @@ class _DataAnalysisState extends State { child: Stack( children: [ Positioned( - top: 0.h, - left: 2.w, - right: 0.h, + top: 4, + left: 4, + right: 0, child: Row( mainAxisSize: MainAxisSize.max, children: [ - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: Checkbox( - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - activeColor: const Color(0xFFCE525F), - value: isFourierTransformSelected, - onChanged: (bool? value) { - setState( - () { - isFourierTransformSelected = value; - }, - ); - }, - ), - ), - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: const Text( - 'Fourier Analysis', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - fontStyle: FontStyle.normal, - ), - ), + Checkbox( + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + activeColor: const Color(0xFFCE525F), + value: isFourierTransformSelected, + onChanged: (bool? value) { + setState( + () { + isFourierTransformSelected = value; + }, + ); + }, ), - const Spacer(), - DropdownMenu( - width: 50.w, - initialSelection: 'CH1', - dropdownMenuEntries: [ - 'CH1', - 'CH2', - 'CH3', - 'MIC', - ].map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, - ); - }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, - ), - textStyle: const TextStyle( - fontSize: 14, + const Text( + 'Fourier Analysis', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, ), ), ], ), ), Positioned( - bottom: 0.h, - left: 6.w, - right: 0.h, + bottom: -6, + left: 12, + right: 0, child: Row( mainAxisSize: MainAxisSize.max, children: [ DropdownMenu( - width: 80.w, + width: 150, initialSelection: 'Sine Fit', dropdownMenuEntries: [ 'Sine Fit', @@ -115,50 +84,79 @@ class _DataAnalysisState extends State { border: InputBorder.none, ), textStyle: const TextStyle( - fontSize: 14, - ), - ), - const Spacer(), - Padding( - padding: EdgeInsets.only(left: 4.w), - child: DropdownMenu( - width: 50.w, - initialSelection: 'CH1', - dropdownMenuEntries: [ - 'CH1', - 'CH2', - 'CH3', - 'MIC', - ].map( - (String value) { - return DropdownMenuEntry( - label: value, - value: value, - ); - }, - ).toList(), - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, - ), - textStyle: const TextStyle( - fontSize: 14, - ), + fontSize: 15, ), ), ], ), ), + Positioned( + top: -2, + right: 0, + child: DropdownMenu( + width: 90, + initialSelection: '', + dropdownMenuEntries: [ + '', + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 15, + ), + ), + ), + Positioned( + bottom: -6, + right: 0, + child: DropdownMenu( + width: 90, + initialSelection: '', + dropdownMenuEntries: [ + '', + 'CH1', + 'CH2', + 'CH3', + 'MIC', + ].map( + (String value) { + return DropdownMenuEntry( + label: value, + value: value, + ); + }, + ).toList(), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + textStyle: const TextStyle( + fontSize: 15, + ), + ), + ), ], ), ), Positioned( - left: 0.w, - right: 0.w, - top: 2.h, + left: 0, + right: 0, + top: 1, child: Align( alignment: Alignment.center, child: Container( - padding: EdgeInsets.symmetric(horizontal: 2.w), + padding: const EdgeInsets.symmetric(horizontal: 2), decoration: const BoxDecoration(color: Colors.white), child: const Text( 'Data Analysis', @@ -187,12 +185,12 @@ class _DataAnalysisState extends State { child: Stack( children: [ Positioned( - bottom: 0.h, - top: 0.h, - left: 6.w, + bottom: 0, + top: 0, + left: 12, child: Center( child: DropdownMenu( - width: 50.w, + width: 90, initialSelection: 'CH1', dropdownMenuEntries: [ 'CH1', @@ -211,15 +209,15 @@ class _DataAnalysisState extends State { border: InputBorder.none, ), textStyle: const TextStyle( - fontSize: 14, + fontSize: 15, ), ), ), ), Positioned( - top: 4.h, - right: 4.w, - left: 45.w, + top: 4, + right: 8, + left: 75, child: Row( children: [ Expanded( @@ -256,9 +254,9 @@ class _DataAnalysisState extends State { ), ), Positioned( - bottom: 0.h, - right: 4.w, - left: 45.w, + bottom: -2, + right: 8, + left: 75, child: Row( children: [ Expanded( @@ -298,13 +296,13 @@ class _DataAnalysisState extends State { ), ), Positioned( - left: 0.w, - right: 0.w, - top: 2.h, + left: 0, + right: 0, + top: 1, child: Align( alignment: Alignment.center, child: Container( - padding: EdgeInsets.symmetric(horizontal: 2.w), + padding: const EdgeInsets.symmetric(horizontal: 2), decoration: const BoxDecoration(color: Colors.white), child: const Text( 'Offsets', diff --git a/lib/view/widgets/navigation_drawer.dart b/lib/view/widgets/navigation_drawer.dart index dc1051689..3870899d7 100644 --- a/lib/view/widgets/navigation_drawer.dart +++ b/lib/view/widgets/navigation_drawer.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:pslab/providers/board_state_provider.dart'; class NavDrawer extends StatefulWidget { @@ -18,7 +17,7 @@ class _NavDrawerState extends State { @override Widget build(BuildContext context) { return Drawer( - width: 280.w, + width: MediaQuery.of(context).size.width * 0.75, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.zero, ), @@ -56,7 +55,6 @@ class _NavDrawerState extends State { ), ), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -88,7 +86,6 @@ class _NavDrawerState extends State { }, ), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -109,7 +106,6 @@ class _NavDrawerState extends State { ), const Divider(), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -129,7 +125,6 @@ class _NavDrawerState extends State { }, ), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -149,7 +144,6 @@ class _NavDrawerState extends State { }, ), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -170,7 +164,6 @@ class _NavDrawerState extends State { ), const Divider(), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -190,7 +183,6 @@ class _NavDrawerState extends State { }, ), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -210,7 +202,6 @@ class _NavDrawerState extends State { }, ), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -230,7 +221,6 @@ class _NavDrawerState extends State { }, ), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -250,7 +240,6 @@ class _NavDrawerState extends State { }, ), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -270,7 +259,6 @@ class _NavDrawerState extends State { }, ), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -290,7 +278,6 @@ class _NavDrawerState extends State { }, ), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( @@ -310,7 +297,6 @@ class _NavDrawerState extends State { }, ), ListTile( - minLeadingWidth: 30.w, focusColor: Colors.grey[350], dense: true, leading: Icon( diff --git a/lib/view/widgets/oscilloscope_screen_tabs.dart b/lib/view/widgets/oscilloscope_screen_tabs.dart index 16c8dd1e9..184b01f6a 100644 --- a/lib/view/widgets/oscilloscope_screen_tabs.dart +++ b/lib/view/widgets/oscilloscope_screen_tabs.dart @@ -68,7 +68,7 @@ class _OscilloscopeTabsState extends State { maxLines: 1, style: TextStyle( color: Colors.black, - fontSize: 9, + fontSize: 10, fontStyle: FontStyle.normal, fontWeight: FontWeight.bold, ), @@ -120,7 +120,7 @@ class _OscilloscopeTabsState extends State { maxLines: 1, style: TextStyle( color: Colors.black, - fontSize: 9, + fontSize: 10, fontStyle: FontStyle.normal, fontWeight: FontWeight.bold, ), @@ -171,7 +171,7 @@ class _OscilloscopeTabsState extends State { maxLines: 1, style: TextStyle( color: Colors.black, - fontSize: 9, + fontSize: 10, fontStyle: FontStyle.normal, fontWeight: FontWeight.bold, ), @@ -222,7 +222,7 @@ class _OscilloscopeTabsState extends State { maxLines: 1, style: TextStyle( color: Colors.black, - fontSize: 9, + fontSize: 10, fontStyle: FontStyle.normal, fontWeight: FontWeight.bold, ), diff --git a/lib/view/widgets/timebase_trigger_widget.dart b/lib/view/widgets/timebase_trigger_widget.dart index 7f65ae91d..50b697ee5 100644 --- a/lib/view/widgets/timebase_trigger_widget.dart +++ b/lib/view/widgets/timebase_trigger_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; class TimebaseTriggerWidget extends StatefulWidget { const TimebaseTriggerWidget({super.key}); @@ -26,41 +25,35 @@ class _TimebaseTriggerState extends State { child: Stack( children: [ Positioned( - top: 0.h, - left: 2.w, - right: 0.w, + bottom: -4, + left: 4, + right: 0, child: Row( children: [ - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - activeColor: const Color(0xFFCE525F), - value: isTriggerChecked, - onChanged: (bool? value) { - setState( - () { - isTriggerChecked = value; - }, - ); - }, - ), + Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + activeColor: const Color(0xFFCE525F), + value: isTriggerChecked, + onChanged: (bool? value) { + setState( + () { + isTriggerChecked = value; + }, + ); + }, ), - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: const Text( - 'Trigger', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - fontStyle: FontStyle.normal, - ), + const Text( + 'Trigger', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, ), ), Padding( - padding: EdgeInsets.only(left: 8.w), + padding: const EdgeInsets.only(left: 8), child: DropdownMenu( - width: 50.w, + width: 90, initialSelection: 'CH1', dropdownMenuEntries: [ 'CH1', @@ -79,7 +72,7 @@ class _TimebaseTriggerState extends State { border: InputBorder.none, ), textStyle: const TextStyle( - fontSize: 14, + fontSize: 15, ), ), ), @@ -114,9 +107,9 @@ class _TimebaseTriggerState extends State { ), ), Padding( - padding: EdgeInsets.only(left: 8.w), + padding: const EdgeInsets.only(left: 32), child: DropdownMenu( - width: 70.w, + width: 150, initialSelection: 'Rising Edge', dropdownMenuEntries: [ 'Rising Edge', @@ -142,9 +135,9 @@ class _TimebaseTriggerState extends State { ), ), Positioned( - bottom: 0.h, - left: 8.w, - right: 8.w, + top: -2, + left: 16, + right: 16, child: Row( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, @@ -152,7 +145,7 @@ class _TimebaseTriggerState extends State { const Text( 'Timebase', style: TextStyle( - fontSize: 14, + fontSize: 15, fontWeight: FontWeight.bold, fontStyle: FontStyle.normal, ), @@ -195,13 +188,13 @@ class _TimebaseTriggerState extends State { ), ), Positioned( - left: 0.w, - right: 0.w, - top: 2.h, + left: 0, + right: 0, + top: 1, child: Align( alignment: Alignment.center, child: Container( - padding: EdgeInsets.symmetric(horizontal: 2.w), + padding: const EdgeInsets.symmetric(horizontal: 2), decoration: const BoxDecoration(color: Colors.white), child: const Text( 'Timebase & Trigger', diff --git a/lib/view/widgets/xyplot_widget.dart b/lib/view/widgets/xyplot_widget.dart index d5d9082e6..8697257a2 100644 --- a/lib/view/widgets/xyplot_widget.dart +++ b/lib/view/widgets/xyplot_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; class XYPlotWidget extends StatefulWidget { const XYPlotWidget({super.key}); @@ -24,41 +23,44 @@ class _XYPlotState extends State { child: Stack( children: [ Positioned( - top: 0.h, - left: 2.w, - right: 0.w, + top: 4, + left: 4, + right: 0, child: Row( mainAxisSize: MainAxisSize.max, children: [ - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - activeColor: const Color(0xFFCE525F), - value: isXYPlotSelected, - onChanged: (bool? value) { - setState( - () { - isXYPlotSelected = value; - }, - ); - }, - ), + Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + activeColor: const Color(0xFFCE525F), + value: isXYPlotSelected, + onChanged: (bool? value) { + setState( + () { + isXYPlotSelected = value; + }, + ); + }, ), - Padding( - padding: EdgeInsets.only(bottom: 2.h), - child: const Text( - 'Enable XY Plot', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - fontStyle: FontStyle.normal, - ), + const Text( + 'Enable XY Plot', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, ), ), - const Spacer(), + ], + ), + ), + Positioned( + bottom: -6, + left: 12, + right: 12, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ DropdownMenu( - width: 50.w, + width: 90, initialSelection: 'CH1', dropdownMenuEntries: [ 'CH1', @@ -77,11 +79,11 @@ class _XYPlotState extends State { border: InputBorder.none, ), textStyle: const TextStyle( - fontSize: 14, + fontSize: 15, ), ), DropdownMenu( - width: 50.w, + width: 90, initialSelection: 'CH2', dropdownMenuEntries: [ 'CH1', @@ -100,23 +102,23 @@ class _XYPlotState extends State { border: InputBorder.none, ), textStyle: const TextStyle( - fontSize: 14, + fontSize: 15, ), ), ], ), - ), + ) ], ), ), Positioned( - left: 0.w, - right: 0.w, - top: 2.h, + left: 0, + right: 0, + top: 1, child: Align( alignment: Alignment.center, child: Container( - padding: EdgeInsets.symmetric(horizontal: 2.w), + padding: const EdgeInsets.symmetric(horizontal: 2), decoration: const BoxDecoration(color: Colors.white), child: const Text( 'XY Plot', diff --git a/pubspec.lock b/pubspec.lock index aa5f3f66e..0d364ae21 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + data: + dependency: "direct main" + description: + name: data + sha256: eb69f565061178da682b57e07468ff284e1156322ae48f0e7270e466943ea0df + url: "https://pub.dev" + source: hosted + version: "0.13.0" equatable: dependency: transitive description: @@ -93,15 +101,24 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: "10ddaf334fe84d59333a12d153043e366f243e0bdfff2df0313e1e249f5bf926" + sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237" url: "https://pub.dev" source: hosted - version: "0.70.1" + version: "0.70.2" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_audio_capture: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: "1cbaaa082d3158eb7c605789f48a3fc4e9eb3b14" + url: "https://github.com/AsCress/flutter_audio_capture.git" + source: git + version: "1.1.8" flutter_lints: dependency: "direct dev" description: @@ -110,14 +127,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - flutter_screenutil: - dependency: "direct main" - description: - name: flutter_screenutil - sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de" - url: "https://pub.dev" - source: hosted - version: "5.9.3" flutter_svg: dependency: "direct main" description: @@ -151,10 +160,10 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_parser: dependency: transitive description: @@ -219,6 +228,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + more: + dependency: transitive + description: + name: more + sha256: "7589f39d4e5858213e80454844c6f0b1ef58e254e6614fe387e2029fc325efad" + url: "https://pub.dev" + source: hosted + version: "4.4.0" nested: dependency: transitive description: @@ -315,14 +332,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - polynomial: - dependency: "direct main" - description: - name: polynomial - sha256: "2a2e58e70a131a02d6073ff11d56dde8392d76d075ffdd038c067fc3e8258733" - url: "https://pub.dev" - source: hosted - version: "1.0.0" provider: dependency: "direct main" description: @@ -397,7 +406,7 @@ packages: description: path: "." ref: master - resolved-ref: "35f3cca1454f03f07bb646bfb36aed32011e5443" + resolved-ref: ba7892bca3b8bf7725af92c6cc58279659c2a2bc url: "https://github.com/AsCress/usbserial.git" source: git version: "0.5.1" @@ -466,5 +475,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.5.4 <4.0.0" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index f0ae39912..676b42de1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,6 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 - flutter_screenutil: ^5.9.3 flutter_svg: ^2.0.17 google_fonts: ^6.2.1 fl_chart: ^0.70.1 @@ -44,8 +43,12 @@ dependencies: git: url: https://github.com/AsCress/usbserial.git ref: master - polynomial: 1.0.0 get_it: ^8.0.3 + flutter_audio_capture: + git: + url: https://github.com/AsCress/flutter_audio_capture.git + ref: master + data: ^0.13.0 dev_dependencies: flutter_test: