diff --git a/lib/communication/commands_proto.dart b/lib/communication/commands_proto.dart index 7d6cdeb88..a6c64c4c7 100644 --- a/lib/communication/commands_proto.dart +++ b/lib/communication/commands_proto.dart @@ -72,6 +72,7 @@ class CommandsProto { int dac = 6; int setDac = 1; int setCalibratedDac = 2; + int setPower = 3; int wavegen = 7; int setWg = 1; diff --git a/lib/communication/peripherals/dac_channel.dart b/lib/communication/peripherals/dac_channel.dart new file mode 100644 index 000000000..9b6061cc2 --- /dev/null +++ b/lib/communication/peripherals/dac_channel.dart @@ -0,0 +1,27 @@ +import 'package:data/polynomial.dart'; +import 'package:data/type.dart'; + +class DACChannel { + late String name; + late int channum; + late int offset; + late List range; + late double slope; + late double intercept; + late Polynomial vToCode; + late Polynomial codeToV; + late int channelCode; + late String calibrationEnabled; + + DACChannel(this.name, List span, this.channum, this.channelCode) { + range = span; + slope = span[1] - span[0]; + intercept = span[0]; + vToCode = Polynomial.fromCoefficients( + DataType.float, [3300.0 / slope, -3300.0 * intercept / slope]); + codeToV = Polynomial.fromCoefficients( + DataType.float, [slope / 3300.0, intercept]); + calibrationEnabled = "false"; + offset = 0; + } +} diff --git a/lib/communication/science_lab.dart b/lib/communication/science_lab.dart index 85952178e..8971400cc 100644 --- a/lib/communication/science_lab.dart +++ b/lib/communication/science_lab.dart @@ -5,8 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:pslab/communication/commands_proto.dart'; import 'package:pslab/communication/handler/base.dart'; import 'package:pslab/communication/packet_handler.dart'; +import 'package:pslab/communication/peripherals/dac_channel.dart'; import 'package:pslab/communication/socket_client.dart'; import 'package:pslab/others/logger_service.dart'; +import 'package:pslab/providers/board_state_provider.dart'; import 'package:pslab/providers/locator.dart'; import 'analogChannel/analog_acquisition_channel.dart'; @@ -35,6 +37,7 @@ class ScienceLab { Map waveType = {}; List aChannels = []; List dChannels = []; + Map dacChannels = {}; static final double capacitorDischargeVoltage = 0.01 * 3.3; late CommunicationHandler mCommunicationHandler; @@ -115,6 +118,17 @@ class ScienceLab { squareWaveFrequency['SQR2'] = 0.0; squareWaveFrequency['SQR3'] = 0.0; squareWaveFrequency['SQR4'] = 0.0; + if (getIt().pslabVersion == 6) { + dacChannels['PCS'] = DACChannel('PCS', [0, 3.3], 0, 0); + dacChannels['PV3'] = DACChannel('PV3', [0, 3.3], 1, 1); + dacChannels['PV2'] = DACChannel('PV2', [-3.3, 3.3], 2, 0); + dacChannels['PV1'] = DACChannel('PV1', [-5, 5], 3, 1); + } else { + dacChannels['PCS'] = DACChannel('PCS', [0, 3.3], 0, 0); + dacChannels['PV3'] = DACChannel('PV3', [0, 3.3], 1, 1); + dacChannels['PV2'] = DACChannel('PV2', [-3.3, 3.3], 2, 2); + dacChannels['PV1'] = DACChannel('PV1', [-5, 5], 3, 3); + } if (isConnected()) { await runInitSequence(true); @@ -1024,6 +1038,58 @@ class ScienceLab { return null; } + Future setVoltage(String channel, double voltage) async { + DACChannel dacChannel = dacChannels[channel]!; + int v = dacChannel.vToCode(voltage).toInt(); + try { + mPacketHandler.sendByte(mCommandsProto.dac); + mPacketHandler.sendByte(mCommandsProto.setPower); + mPacketHandler.sendByte(dacChannel.channelCode); + mPacketHandler.sendInt(v); + await mPacketHandler.getAcknowledgement(); + } catch (e) { + logger.e("Error in setVoltage: $e"); + } + } + + Future setCurrent(double current) async { + DACChannel dacChannel = dacChannels['PCS']!; + int v = (3300 - dacChannel.vToCode(current)).toInt(); + try { + mPacketHandler.sendByte(mCommandsProto.dac); + mPacketHandler.sendByte(mCommandsProto.setPower); + mPacketHandler.sendByte(dacChannel.channelCode); + mPacketHandler.sendInt(v); + await mPacketHandler.getAcknowledgement(); + } catch (e) { + logger.e("Error in setCurrent: $e"); + } + } + + Future setPV1(double voltage) async { + if (isConnected()) { + await setVoltage('PV1', voltage); + } + } + + Future setPV2(double voltage) async { + if (isConnected()) { + await setVoltage('PV2', voltage); + } + } + + Future setPV3(double voltage) async { + if (isConnected()) { + await setVoltage('PV3', voltage); + } + } + + Future setPCS(double current) async { + if (isConnected()) { + await setCurrent(current); + } + } + Future servo4( double? angle1, double? angle2, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3f2278495..5e1e2c466 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -94,6 +94,11 @@ "analysisOptionEveryRisingEdge": "EVERY RISING EDGE", "analysisOptionEveryFourthRisingEdge": "EVERY FOURTH RISING EDGE", "analysisOptionDisabled": "DISABLED", + "powerSourceTitle": "Power Source", + "pinPV1": "PV1", + "pinPV2": "PV2", + "pinPV3": "PV3", + "pinPCS": "PCS", "analyze": "ANALYZE", "settings": "Settings", "autoStart": "Auto Start", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1f83d13e8..5ce461ad9 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -658,6 +658,36 @@ abstract class AppLocalizations { /// **'DISABLED'** String get analysisOptionDisabled; + /// No description provided for @powerSourceTitle. + /// + /// In en, this message translates to: + /// **'Power Source'** + String get powerSourceTitle; + + /// No description provided for @pinPV1. + /// + /// In en, this message translates to: + /// **'PV1'** + String get pinPV1; + + /// No description provided for @pinPV2. + /// + /// In en, this message translates to: + /// **'PV2'** + String get pinPV2; + + /// No description provided for @pinPV3. + /// + /// In en, this message translates to: + /// **'PV3'** + String get pinPV3; + + /// No description provided for @pinPCS. + /// + /// In en, this message translates to: + /// **'PCS'** + String get pinPCS; + /// No description provided for @analyze. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 04d10402b..2a05bbfda 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -301,6 +301,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get analysisOptionDisabled => 'DISABLED'; + @override + String get powerSourceTitle => 'Power Source'; + + @override + String get pinPV1 => 'PV1'; + + @override + String get pinPV2 => 'PV2'; + + @override + String get pinPV3 => 'PV3'; + + @override + String get pinPCS => 'PCS'; + @override String get analyze => 'ANALYZE'; diff --git a/lib/main.dart b/lib/main.dart index 6a7ac6458..0b8cd90ae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,7 @@ import 'package:pslab/view/logic_analyzer_screen.dart'; import 'package:pslab/view/luxmeter_screen.dart'; import 'package:pslab/view/multimeter_screen.dart'; import 'package:pslab/view/oscilloscope_screen.dart'; +import 'package:pslab/view/power_source_screen.dart'; import 'package:pslab/view/robotic_arm_screen.dart'; import 'package:pslab/view/settings_screen.dart'; import 'package:pslab/view/about_us_screen.dart'; @@ -60,6 +61,7 @@ class MyApp extends StatelessWidget { '/oscilloscope': (context) => const OscilloscopeScreen(), '/multimeter': (context) => const MultimeterScreen(), '/logicAnalyzer': (context) => const LogicAnalyzerScreen(), + '/powerSource': (context) => const PowerSourceScreen(), '/connectDevice': (context) => const ConnectDeviceScreen(), '/faq': (context) => FAQScreen(), '/settings': (context) => const SettingsScreen(), diff --git a/lib/providers/board_state_provider.dart b/lib/providers/board_state_provider.dart index 0f6c4c6b2..28c18399f 100644 --- a/lib/providers/board_state_provider.dart +++ b/lib/providers/board_state_provider.dart @@ -15,6 +15,9 @@ class BoardStateProvider extends ChangeNotifier { bool hasPermission = false; late ScienceLabCommon scienceLabCommon; String pslabVersionID = 'Not Connected'; + String pslabVersionIDV6 = 'PSLab V6'; + String pslabVersionIDV5 = 'PSLab V5'; + int pslabVersion = 0; late String exportFormat; bool autoStart = true; @@ -67,6 +70,11 @@ class BoardStateProvider extends ChangeNotifier { Future setPSLabVersionIDs() async { pslabVersionID = await getIt.get().getVersion(); + if (pslabVersionID == pslabVersionIDV6) { + pslabVersion = 6; + } else if (pslabVersionID == pslabVersionIDV5) { + pslabVersion = 5; + } notifyListeners(); } diff --git a/lib/providers/power_source_state_provider.dart b/lib/providers/power_source_state_provider.dart new file mode 100644 index 000000000..c1ac92a05 --- /dev/null +++ b/lib/providers/power_source_state_provider.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:pslab/communication/science_lab.dart'; +import 'package:pslab/providers/locator.dart'; + +enum Pin { + pv1, + pv2, + pv3, + pcs, +} + +class PowerSourceStateProvider extends ChangeNotifier { + late double voltagePV1; + late double voltagePV2; + late double voltagePV3; + late double currentPCS; + + late List rangePV1; + late List rangePV2; + late List rangePV3; + late List rangePCS; + + late double step; + + late ScienceLab _scienceLab; + + PowerSourceStateProvider() { + voltagePV1 = -5.00; + voltagePV2 = -3.30; + voltagePV3 = 0.00; + currentPCS = 0.00; + + rangePV1 = [-5.00, 5.00]; + rangePV2 = [-3.30, 3.30]; + rangePV3 = [0.00, 3.30]; + rangePCS = [0.00, 3.30]; + + step = 0.01; + + _scienceLab = getIt.get(); + } + + double valueToIndex(double value, Pin pin) { + List range; + int sections; + switch (pin) { + case Pin.pv1: + range = rangePV1; + sections = 1000; + break; + case Pin.pv2: + range = rangePV2; + sections = 660; + break; + case Pin.pv3: + range = rangePV3; + sections = 330; + break; + case Pin.pcs: + range = rangePCS; + sections = 330; + break; + } + final clampedValue = value.clamp(range[0], range[1]); + return ((clampedValue - range[0]) / (range[1] - range[0])) * sections; + } + + double indexToValue(double index, Pin pin) { + List range; + int sections; + switch (pin) { + case Pin.pv1: + range = rangePV1; + sections = 1000; + break; + case Pin.pv2: + range = rangePV2; + sections = 660; + break; + case Pin.pv3: + range = rangePV3; + sections = 330; + break; + case Pin.pcs: + range = rangePCS; + sections = 330; + break; + } + final clampedIndex = index.clamp(0, sections); + return (clampedIndex / sections) * (range[1] - range[0]) + range[0]; + } + + Future setPV1(double value) async { + final clampedValue = value.clamp(rangePV1[0], rangePV1[1]); + voltagePV1 = clampedValue; + voltagePV3 = (3.3 / 2) * (1 + (voltagePV1 / 5.0)); + await _scienceLab.setPV1(voltagePV1); + notifyListeners(); + } + + Future setPV2(double value) async { + final clampedValue = value.clamp(rangePV2[0], rangePV2[1]); + voltagePV2 = clampedValue; + currentPCS = (3.3 - voltagePV2) / 2; + await _scienceLab.setPV2(voltagePV2); + notifyListeners(); + } + + Future setPV3(double value) async { + final clampedValue = value.clamp(rangePV3[0], rangePV3[1]); + voltagePV3 = clampedValue; + voltagePV1 = 5 * (2 * voltagePV3 / 3.3 - 1); + await _scienceLab.setPV3(voltagePV3); + notifyListeners(); + } + + Future setPCS(double value) async { + final clampedValue = value.clamp(rangePCS[0], rangePCS[1]); + currentPCS = clampedValue; + voltagePV2 = 3.3 - 2 * currentPCS; + await _scienceLab.setPCS(currentPCS); + notifyListeners(); + } + + Future setValue(double value, Pin pin) async { + switch (pin) { + case Pin.pv1: + await setPV1(value); + break; + case Pin.pv2: + await setPV2(value); + break; + case Pin.pv3: + await setPV3(value); + break; + case Pin.pcs: + await setPCS(value); + break; + } + } + + double getValue(Pin pin) { + switch (pin) { + case Pin.pv1: + return voltagePV1; + case Pin.pv2: + return voltagePV2; + case Pin.pv3: + return voltagePV3; + case Pin.pcs: + return currentPCS; + } + } +} diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 232babc72..2af52f35e 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -76,3 +76,5 @@ List logicAnalyzerChannelColors = [ Color logicAnalyzerChannelsTextColor = Colors.black; Color logicAnalyzerTextColor = Colors.white; Color logicAnalyzerGraphTextColor = Colors.amber; +Color powerSourceBorderLightRed = Color.fromARGB(255, 240, 162, 162); +Color powerSourceKnobColor = Colors.grey.shade100; diff --git a/lib/view/instruments_screen.dart b/lib/view/instruments_screen.dart index 57d5b7b66..3f8e4d564 100644 --- a/lib/view/instruments_screen.dart +++ b/lib/view/instruments_screen.dart @@ -57,6 +57,18 @@ class _InstrumentsScreenState extends State { ); } break; + case 5: + if (Navigator.canPop(context) && + ModalRoute.of(context)?.settings.name == '/powerSource') { + Navigator.popUntil(context, ModalRoute.withName('/powerSource')); + } else { + Navigator.pushNamedAndRemoveUntil( + context, + '/powerSource', + (route) => route.isFirst, + ); + } + break; case 6: if (Navigator.canPop(context) && ModalRoute.of(context)?.settings.name == '/luxmeter') { diff --git a/lib/view/power_source_screen.dart b/lib/view/power_source_screen.dart new file mode 100644 index 000000000..dc3fb20e8 --- /dev/null +++ b/lib/view/power_source_screen.dart @@ -0,0 +1,496 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/l10n/app_localizations.dart'; +import 'package:pslab/providers/locator.dart'; +import 'package:pslab/providers/power_source_state_provider.dart'; +import 'package:pslab/theme/colors.dart'; +import 'package:pslab/view/widgets/common_scaffold_widget.dart'; +import 'package:pslab/view/widgets/power_source_knob.dart'; + +class PowerSourceScreen extends StatelessWidget { + const PowerSourceScreen({super.key}); + + @override + Widget build(BuildContext context) { + AppLocalizations appLocalizations = getIt.get(); + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => PowerSourceStateProvider()), + ], + child: Consumer( + builder: (context, provider, _) { + return CommonScaffold( + title: appLocalizations.powerSourceTitle, + body: ScrollConfiguration( + behavior: ScrollBehavior(), + child: ListView( + children: [ + Card( + color: scaffoldBackgroundColor, + child: Row( + children: [ + Expanded( + flex: 45, + child: Container( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appLocalizations.pinPV1, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + TextField( + controller: TextEditingController( + text: + '${provider.voltagePV1.toStringAsFixed(2)} V', + ), + style: TextStyle( + fontSize: 18, + ), + textAlign: TextAlign.center, + onSubmitted: (value) async { + String powerValue = + value.replaceAll("V", "").trim(); + double parsedValue = + double.tryParse(powerValue) ?? 0.0; + await provider.setPV1(parsedValue); + }, + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: powerSourceBorderLightRed, + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: powerSourceBorderLightRed, + ), + ), + ), + ), + const SizedBox(height: 20), + Align( + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + height: 50, + width: 55, + child: IconButton.filled( + icon: Icon(Icons.arrow_drop_up), + iconSize: 36, + color: scaffoldBackgroundColor, + onPressed: () async { + await provider.setPV1( + provider.voltagePV1 + + provider.step); + }, + style: IconButton.styleFrom( + backgroundColor: primaryRed, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(4), + ), + ), + ), + ), + SizedBox( + height: 50, + width: 55, + child: IconButton.filled( + icon: Icon(Icons.arrow_drop_down), + iconSize: 36, + color: scaffoldBackgroundColor, + onPressed: () async { + await provider.setPV1( + provider.voltagePV1 - + provider.step); + }, + style: IconButton.styleFrom( + backgroundColor: primaryRed, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(4), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + Expanded( + flex: 55, + child: PowerSourceKnob( + maxValue: 1000, + pin: Pin.pv1, + ), + ) + ], + ), + ), + Card( + color: scaffoldBackgroundColor, + child: Row( + children: [ + Expanded( + flex: 45, + child: Container( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appLocalizations.pinPV2, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + TextField( + controller: TextEditingController( + text: + '${provider.voltagePV2.toStringAsFixed(2)} V', + ), + textAlign: TextAlign.center, + onSubmitted: (value) async { + String powerValue = + value.replaceAll("V", "").trim(); + double parsedValue = + double.tryParse(powerValue) ?? 0.0; + await provider.setPV2(parsedValue); + }, + style: TextStyle( + fontSize: 18, + ), + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: powerSourceBorderLightRed, + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: powerSourceBorderLightRed, + ), + ), + ), + ), + const SizedBox(height: 20), + Align( + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + height: 50, + width: 55, + child: IconButton.filled( + icon: Icon(Icons.arrow_drop_up), + iconSize: 36, + color: scaffoldBackgroundColor, + onPressed: () async { + await provider.setPV2( + provider.voltagePV2 + + provider.step); + }, + style: IconButton.styleFrom( + backgroundColor: primaryRed, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(4), + ), + ), + ), + ), + SizedBox( + height: 50, + width: 55, + child: IconButton.filled( + icon: Icon(Icons.arrow_drop_down), + iconSize: 36, + color: scaffoldBackgroundColor, + onPressed: () async { + await provider.setPV2( + provider.voltagePV2 - + provider.step); + }, + style: IconButton.styleFrom( + backgroundColor: primaryRed, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(4), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + Expanded( + flex: 55, + child: PowerSourceKnob( + maxValue: 660, + pin: Pin.pv2, + ), + ) + ], + ), + ), + Card( + color: scaffoldBackgroundColor, + child: Row( + children: [ + Expanded( + flex: 45, + child: Container( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appLocalizations.pinPV3, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + TextField( + controller: TextEditingController( + text: + '${provider.voltagePV3.toStringAsFixed(2)} V', + ), + textAlign: TextAlign.center, + onSubmitted: (value) async { + String powerValue = + value.replaceAll("V", "").trim(); + double parsedValue = + double.tryParse(powerValue) ?? 0.0; + await provider.setPV3(parsedValue); + }, + style: TextStyle( + fontSize: 18, + ), + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: powerSourceBorderLightRed, + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: powerSourceBorderLightRed, + ), + ), + ), + ), + const SizedBox(height: 20), + Align( + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + height: 50, + width: 55, + child: IconButton.filled( + icon: Icon(Icons.arrow_drop_up), + iconSize: 36, + color: scaffoldBackgroundColor, + onPressed: () async { + await provider.setPV3( + provider.voltagePV3 + + provider.step); + }, + style: IconButton.styleFrom( + backgroundColor: primaryRed, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(4), + ), + ), + ), + ), + SizedBox( + height: 50, + width: 55, + child: IconButton.filled( + icon: Icon(Icons.arrow_drop_down), + iconSize: 36, + color: scaffoldBackgroundColor, + onPressed: () async { + await provider.setPV3( + provider.voltagePV3 - + provider.step); + }, + style: IconButton.styleFrom( + backgroundColor: primaryRed, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(4), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + Expanded( + flex: 55, + child: PowerSourceKnob( + maxValue: 330, + pin: Pin.pv3, + ), + ) + ], + ), + ), + Card( + color: scaffoldBackgroundColor, + child: Row( + children: [ + Expanded( + flex: 45, + child: Container( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appLocalizations.pinPCS, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + TextField( + controller: TextEditingController( + text: + '${provider.currentPCS.toStringAsFixed(2)} mA', + ), + style: TextStyle( + fontSize: 18, + ), + textAlign: TextAlign.center, + onSubmitted: (value) async { + String powerValue = + value.replaceAll("V", "").trim(); + double parsedValue = + double.tryParse(powerValue) ?? 0.0; + await provider.setPCS(parsedValue); + }, + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: powerSourceBorderLightRed, + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: powerSourceBorderLightRed, + ), + ), + ), + ), + const SizedBox(height: 20), + Align( + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + height: 50, + width: 55, + child: IconButton.filled( + icon: Icon(Icons.arrow_drop_up), + iconSize: 36, + color: scaffoldBackgroundColor, + onPressed: () async { + await provider.setPCS( + provider.currentPCS + + provider.step); + }, + style: IconButton.styleFrom( + backgroundColor: primaryRed, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(4), + ), + ), + ), + ), + SizedBox( + height: 50, + width: 55, + child: IconButton.filled( + icon: Icon(Icons.arrow_drop_down), + iconSize: 36, + color: scaffoldBackgroundColor, + onPressed: () async { + await provider.setPCS( + provider.currentPCS - + provider.step); + }, + style: IconButton.styleFrom( + backgroundColor: primaryRed, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(4), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + Expanded( + flex: 55, + child: PowerSourceKnob( + maxValue: 330, + pin: Pin.pcs, + ), + ) + ], + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/view/widgets/power_source_knob.dart b/lib/view/widgets/power_source_knob.dart new file mode 100644 index 000000000..bff0811ba --- /dev/null +++ b/lib/view/widgets/power_source_knob.dart @@ -0,0 +1,257 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/providers/power_source_state_provider.dart'; +import 'package:pslab/theme/colors.dart'; + +class InnerDialPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = min(size.width / 2, size.height / 2) * 0.75; + + final paint = Paint() + ..color = powerSourceKnobColor + ..style = PaintingStyle.stroke + ..strokeWidth = 10; + + canvas.drawCircle(center, radius, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +class RadialDialPainter extends CustomPainter { + final double value; + final double max; + final Color color; + + RadialDialPainter({ + required this.value, + required this.max, + required this.color, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = min(size.width / 2, size.height / 2) * 0.9; + + final paint = Paint() + ..color = powerSourceKnobColor + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.square + ..strokeWidth = 4; + + const startAngle = 3 * pi / 4; + + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + startAngle, + 6 * pi / 4, + false, + paint, + ); + + final progressPaint = Paint() + ..color = primaryRed + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.square + ..strokeWidth = 9; + + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + startAngle, + 6 * pi / 4 * (value / max), + false, + progressPaint, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} + +class InnerPointerPainter extends CustomPainter { + final double value; + final double max; + final Color color; + + InnerPointerPainter({ + required this.value, + required this.max, + required this.color, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = min(size.width / 2, size.height / 2) * 0.5; + + final pointerAngle = 3 * pi / 4 + 6 * pi / 4 * (value / max); + final pointerLength = radius + 15; + + final pointerPaint = Paint() + ..color = color + ..strokeCap = StrokeCap.square + ..strokeWidth = 4; + + final pointerStart = Offset( + center.dx + radius * cos(pointerAngle), + center.dy + radius * sin(pointerAngle), + ); + final pointerEnd = Offset( + center.dx + pointerLength * cos(pointerAngle), + center.dy + pointerLength * sin(pointerAngle), + ); + + canvas.drawLine(pointerStart, pointerEnd, pointerPaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} + +class PowerSourceKnob extends StatefulWidget { + const PowerSourceKnob({super.key, required this.maxValue, required this.pin}); + + final double maxValue; + final Pin pin; + + @override + // ignore: library_private_types_in_public_api + _PowerSourceKnobState createState() => _PowerSourceKnobState(); +} + +class _PowerSourceKnobState extends State { + double outerValue = 0.0; + late final double maxValue = widget.maxValue; + + final double initialAngle = 150 * pi / 180; + double previousAngle = 0.0; + bool isDragging = true; + + @override + void initState() { + super.initState(); + previousAngle = initialAngle; + } + + @override + Widget build(BuildContext context) { + PowerSourceStateProvider powerSourceStateProvider = + Provider.of(context); + outerValue = powerSourceStateProvider.valueToIndex( + powerSourceStateProvider.getValue(widget.pin), + widget.pin, + ); + void updateOuterValue(double angle) { + const startAngle = 155 * pi / 180; + const endAngle = 355 * pi / 180; + + const totalAngle = endAngle - startAngle; + + final numSections = maxValue; + + final anglePerSection = totalAngle / numSections; + + final section = ((angle - startAngle) / anglePerSection).round(); + + final clampedSection = section.clamp(0, numSections); + + setState(() async { + outerValue = clampedSection.toDouble(); + await powerSourceStateProvider.setValue( + powerSourceStateProvider.indexToValue( + outerValue, + widget.pin, + ), + widget.pin); + }); + } + + void updateAngle(Offset position, Size size) { + if (!isDragging) return; + + final center = Offset(size.width / 2, size.height / 2); + final dx = position.dx - center.dx; + final dy = position.dy - center.dy; + final distanceFromCenter = sqrt(dx * dx + dy * dy); + + if (distanceFromCenter > size.width / 2) return; + + var angle = atan2(dy, dx); + + if (angle < 0) { + angle += 2 * pi; + } + + const startAngle = 150 * pi / 180; + const endAngle = 360 * pi / 180; + + if (angle >= startAngle && angle <= endAngle) { + if ((angle >= previousAngle && angle <= endAngle) || + (angle < startAngle && previousAngle < startAngle) || + (angle - previousAngle).abs() < pi) { + setState(() { + updateOuterValue(angle); + }); + } + previousAngle = angle; + } + } + + return Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + painter: RadialDialPainter( + value: outerValue, + max: maxValue, + color: primaryRed, + ), + child: SizedBox( + width: 200, + height: 200, + ), + ), + CustomPaint( + painter: InnerDialPainter(), + child: SizedBox( + width: 180, + height: 180, + ), + ), + GestureDetector( + onPanUpdate: (details) { + if (isDragging) { + RenderBox renderBox = context.findRenderObject() as RenderBox; + Offset localPosition = + renderBox.globalToLocal(details.globalPosition); + updateAngle(localPosition, renderBox.size); + } + }, + child: CustomPaint( + painter: InnerPointerPainter( + value: outerValue, + max: maxValue, + color: primaryRed, + ), + child: SizedBox( + width: 140, + height: 140, + ), + ), + ), + ], + ); + } +}