From b06280bacaf1a204e349e9e35e37f2773f08b0a3 Mon Sep 17 00:00:00 2001 From: Anashuman Singh Date: Sun, 10 Aug 2025 21:58:13 +0530 Subject: [PATCH 1/2] feat: added functionality for Wave Generator --- assets/icons/ic_pwm_pic.png | Bin 0 -> 1386 bytes lib/communication/science_lab.dart | 278 ++++++++++++++- lib/l10n/app_en.arb | 7 + lib/l10n/app_localizations.dart | 42 +++ lib/l10n/app_localizations_en.dart | 21 ++ lib/others/wave_generator_constants.dart | 41 +++ .../wave_generator_state_provider.dart | 332 +++++++++++++++++ lib/theme/colors.dart | 3 + lib/view/wave_generator_screen.dart | 211 ++++++----- .../widgets/analog_waveform_controls.dart | 187 +++++++--- .../widgets/digital_waveform_controls.dart | 238 ++++++++----- lib/view/widgets/wave_generator_graph.dart | 10 +- .../widgets/wave_generator_main_controls.dart | 333 ++++++++++++------ 13 files changed, 1378 insertions(+), 325 deletions(-) create mode 100644 assets/icons/ic_pwm_pic.png create mode 100644 lib/others/wave_generator_constants.dart create mode 100644 lib/providers/wave_generator_state_provider.dart diff --git a/assets/icons/ic_pwm_pic.png b/assets/icons/ic_pwm_pic.png new file mode 100644 index 0000000000000000000000000000000000000000..ec19cfe717f4c68a68b8a528dc1ffde8ac20b9fb GIT binary patch literal 1386 zcmajfjX%?Q00;2zev`30^&r$CT3Ab-vT-JtGqwm7;SgsYMh}uJd0NXWYxB&Mv6H$^ zQ@xbOYo2C`6GtP3kacpS^Dr8=MV$Ks?sc!%=ixtiGrc|C)RDW9004Co(dFdkhyGg? z<;|U7C)fe7#fs!|)aUBR97hrBNm`4+uyFMr0o*4-8Pgm zmZloxa$h%2J&x&`9d;PmR!%b`suIvObrE#EFHUdcmj>kL#P3%}J{|lswY3Wu?J!q7 zBu$i!#|u*j22Dr>sQ=Cze0EOGIOPb%K!Yc0Mrn~76csO6n7V1QvV-*!Jexdk3$3u3v=M}%w1%77|MKoqPxyMm|1!efe3XrH;V zz-_Glx+uUDK;C=6D*rvccPyI9KvS+KHd<6b-lhQ6HnoK44K(`^({9&g{H!g7qX>er zodGJkUXdKnY{ldko;sMvt@bmPR9kxK&_=*j?ImNTtm!LHu|vFGd`GLBuMlH;gq&;2 z84}Gi%W}xl9&F`t5Ttdi6e-QBBd}e9fA^Vmn6$o6ZUP>F_VpZgdDS1noy^p!@i~fG zPLd8W`>WcyC>R4&Nv7CNVL28hWm44f8nu?;=3BDGZHw)4)K&n@EKjyFW#F_GaOJSbuGW|ezr4OP59o!5diE827 zyY_HQ^;E%llmYadg~H0fZxhy9hbp5m&-wh$6csIvdl(7GIT5)&S)bC9e3W_oecT|s z^D{Ek9^e%YJ$|@HdYwi+F9e2>Hbo9V0tp_M?s@b^etC{wW3p6bkkuDbGUx%Cg^Afv zRSD~X??;xX;>uzDw_M$HQ#+4O6lGwTDvl5vrkILUfc`;Rx=9)0Cu^kJ)z%JQS8^~c zZ>@aRZ{6PELOS|UKEm385?kc2nR4u0tP;T9U4Mq4aCS3!#{UtTnC@!|I#XKA)NO8)& z9Knd0TC6t3DeZ;!p4g&Z(2O*X7j-41gNs>>x6_~X)D7tHcEK1+Y1m8tKFTk3@Z3o+ z%9tvRfG_$xcrcdu1??&j6D^1!tk;fuP46b2AYL*)ek60$ zx^U+)y^>m1C2C}5JTQs%qAXy?G#x2$8R=(E`9$@3jJmtkvy=l@l}7uazDX&7Y0);| z!$Le~g~@W$Td@M8kX567ZVqhWbbCpm*91>e-l7HX(_zGE=f0{_qrnc@&Dr=N8pzH7 z>z(~;$8d+I?2`)3X#8wKhUP^_E70*>RtP!(vxx$Q?xk;l1k-oJ@!v4}h2#Yz@IvoN zxf7f=kgt0L!j^6WjE~>1b`sS@mc1J7W((-<;i>%8-XC-sweaWw+ Y;2W setSI1(double frequency, String? waveType) async { + double freqLowLimit = 0.1; + int highRes, tableSize; + + if (frequency < freqLowLimit) { + logger.e("frequency too low"); + return -1; + } else if (frequency < 1100) { + highRes = 1; + tableSize = 512; + } else { + highRes = 0; + tableSize = 32; + } + + if (waveType != null) { + if (waveType == "sine" || waveType == "tria") { + if (this.waveType["SI1"] != waveType) { + loadEquation("SI1", waveType); + } + } else { + logger.e("Not a valid waveform. try sine or tria"); + } + } + + List p = [1, 8, 64, 256]; + int prescalar = 0; + int wavelength = 0; + + while (prescalar <= 3) { + wavelength = (64e6 ~/ frequency ~/ p[prescalar] ~/ tableSize); + frequency = 64e6 / wavelength / p[prescalar] / tableSize; + if (wavelength < 65525) break; + prescalar++; + } + + if (prescalar == 4) { + logger.e("Out of range"); + return -1; + } + + try { + mPacketHandler.sendByte(mCommandsProto.wavegen); + mPacketHandler.sendByte(mCommandsProto.setSine1); + mPacketHandler.sendByte(highRes | (prescalar << 1)); + mPacketHandler.sendInt(wavelength - 1); + await mPacketHandler.getAcknowledgement(); + + sin1Frequency = frequency; + return sin1Frequency; + } catch (e) { + logger.e("Error setting SI1: $e"); + } + + return -1; + } + + Future setSI2(double frequency, String? waveType) async { + double freqLowLimit = 0.1; + int highRes, tableSize; + + if (frequency < freqLowLimit) { + logger.e("frequency too low"); + return -1; + } else if (frequency < 1100) { + highRes = 1; + tableSize = 512; + } else { + highRes = 0; + tableSize = 32; + } + + if (waveType != null) { + if (waveType == "sine" || waveType == "tria") { + if (this.waveType["SI2"] != waveType) { + loadEquation("SI2", waveType); + } + } else { + logger.e("Not a valid waveform. try sine or tria"); + } + } + + List p = [1, 8, 64, 256]; + int prescalar = 0; + int wavelength = 0; + + while (prescalar <= 3) { + wavelength = (64e6 ~/ frequency ~/ p[prescalar] ~/ tableSize); + frequency = 64e6 / wavelength / p[prescalar] / tableSize; + if (wavelength < 65525) break; + prescalar++; + } + + if (prescalar == 4) { + logger.e("Out of range"); + return -1; + } + + try { + mPacketHandler.sendByte(mCommandsProto.wavegen); + mPacketHandler.sendByte(mCommandsProto.setSine2); + mPacketHandler.sendByte(highRes | (prescalar << 1)); + mPacketHandler.sendInt(wavelength - 1); + await mPacketHandler.getAcknowledgement(); + + sin2Frequency = frequency; + return sin2Frequency; + } catch (e) { + logger.e("Error setting SI2: $e"); + } + + return -1; + } + + Future setWaves( + double frequency, double phase, double frequency2) async { + int highRes, tableSize, highRes2, tableSize2; + int wavelength = 0, wavelength2 = 0; + + if (frequency2 == -1) frequency2 = frequency; + + if (frequency < 0.1) { + logger.e("frequency 1 too low"); + return -1; + } else if (frequency < 1100) { + highRes = 1; + tableSize = 512; + } else { + highRes = 0; + tableSize = 32; + } + + if (frequency2 < 0.1) { + logger.e("frequency 2 too low"); + return -1; + } else if (frequency2 < 1100) { + highRes2 = 1; + tableSize2 = 512; + } else { + highRes2 = 0; + tableSize2 = 32; + } + + if (frequency < 1 || frequency2 < 1) { + logger.e( + "extremely low frequencies will have reduced amplitudes due to AC coupling restrictions"); + } + + List p = [1, 8, 64, 256]; + + int prescalar = 0; + double retFrequency = 0; + while (prescalar <= 3) { + wavelength = (64e6 ~/ frequency ~/ p[prescalar] ~/ tableSize); + retFrequency = 64e6 / wavelength / p[prescalar] / tableSize; + if (wavelength < 65525) break; + prescalar++; + } + if (prescalar == 4) { + logger.e("#1 out of range"); + return -1; + } + + int prescalar2 = 0; + double retFrequency2 = 0; + while (prescalar2 <= 3) { + wavelength2 = (64e6 ~/ frequency2 ~/ p[prescalar2] ~/ tableSize2); + retFrequency2 = 64e6 / wavelength2 / p[prescalar2] / tableSize2; + if (wavelength2 < 65525) break; + prescalar2++; + } + if (prescalar2 == 4) { + logger.e("#2 out of range"); + return -1; + } + + int phaseCoarse = (tableSize2 * (phase) / 360).toInt(); + int phaseFine = (wavelength2 * + (phase - (phaseCoarse) * 360 / tableSize2) / + (360 / tableSize2)) + .toInt(); + + try { + mPacketHandler.sendByte(mCommandsProto.wavegen); + mPacketHandler.sendByte(mCommandsProto.setBothWg); + mPacketHandler.sendInt(wavelength - 1); + mPacketHandler.sendInt(wavelength2 - 1); + mPacketHandler.sendInt(phaseCoarse); + mPacketHandler.sendInt(phaseFine); + mPacketHandler.sendByte( + (prescalar2 << 4) | (prescalar << 2) | (highRes2 << 1) | (highRes)); + await mPacketHandler.getAcknowledgement(); + + sin1Frequency = retFrequency; + sin2Frequency = retFrequency2; + return retFrequency; + } catch (e) { + logger.e("Error setting waves: $e"); + } + + return -1; + } + + Future sqrPWM( + double frequency, + double h0, + double p1, + double h1, + double p2, + double h2, + double p3, + double h3, + bool pulse, + ) async { + if (frequency == 0) return -1; + + if (h0 == 0) h0 = 0.1; + if (h1 == 0) h1 = 0.1; + if (h2 == 0) h2 = 0.1; + if (h3 == 0) h3 = 0.1; + + if (frequency > 10e6) { + logger.e( + "Frequency is greater than 10MHz. Please use map_reference_clock for 16 & 32MHz outputs", + ); + return -1; + } + + List p = [1, 8, 64, 256]; + int prescalar = 0; + int wavelength = 0; + + while (prescalar <= 3) { + wavelength = (64e6 ~/ frequency ~/ p[prescalar]); + if (wavelength < 65525) break; + prescalar++; + } + + if (prescalar == 4 || wavelength == 0) { + logger.e("Out of Range"); + return -1; + } + + if (!pulse) prescalar |= (1 << 5); + + int a1 = ((p1 % 1) * wavelength).toInt(); + int b1 = (((h1 + p1) % 1) * wavelength).toInt(); + int a2 = ((p2 % 1) * wavelength).toInt(); + int b2 = (((h2 + p2) % 1) * wavelength).toInt(); + int a3 = ((p3 % 1) * wavelength).toInt(); + int b3 = (((h3 + p3) % 1) * wavelength).toInt(); + + try { + mPacketHandler.sendByte(mCommandsProto.wavegen); + mPacketHandler.sendByte(mCommandsProto.sqr4); + mPacketHandler.sendInt(wavelength - 1); + mPacketHandler.sendInt((wavelength * h0).toInt() - 1); + mPacketHandler.sendInt(a1 > 0 ? a1 - 1 : 0); + mPacketHandler.sendInt(b1 > 1 ? b1 - 1 : 1); + mPacketHandler.sendInt(a2 > 0 ? a2 - 1 : 0); + mPacketHandler.sendInt(b2 > 1 ? b2 - 1 : 1); + mPacketHandler.sendInt(a3 > 0 ? a3 - 1 : 0); + mPacketHandler.sendInt(b3 > 1 ? b3 - 1 : 1); + mPacketHandler.sendByte(prescalar); + await mPacketHandler.getAcknowledgement(); + } catch (e) { + logger.e("Error sending data: $e"); + } + + for (var channel in ["SQR1", "SQR2", "SQR3", "SQR4"]) { + squareWaveFrequency[channel] = 64e6 / wavelength / p[prescalar & 0x3]; + } + + return (64e6 / wavelength / p[prescalar & 0x3]); + } + Future servo4( double? angle1, double? angle2, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1c7bf511a..d8ea17397 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -111,6 +111,13 @@ "phase": "Phase", "duty": "Duty", "produceSound": "Produce Sound", + "frequency": "Frequency", + "phaseOffset": "Phase Offset", + "unitDeg": "°", + "unitPercentage": "%", + "sine": "Sine", + "tri": "Tri", + "pwm": "pwm", "analyze": "Analyze", "settings": "Settings", "autoStart": "Auto Start", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index eed713b84..de9becc01 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -760,6 +760,48 @@ abstract class AppLocalizations { /// **'Produce Sound'** String get produceSound; + /// No description provided for @frequency. + /// + /// In en, this message translates to: + /// **'Frequency'** + String get frequency; + + /// No description provided for @phaseOffset. + /// + /// In en, this message translates to: + /// **'Phase Offset'** + String get phaseOffset; + + /// No description provided for @unitDeg. + /// + /// In en, this message translates to: + /// **'°'** + String get unitDeg; + + /// No description provided for @unitPercentage. + /// + /// In en, this message translates to: + /// **'%'** + String get unitPercentage; + + /// No description provided for @sine. + /// + /// In en, this message translates to: + /// **'Sine'** + String get sine; + + /// No description provided for @tri. + /// + /// In en, this message translates to: + /// **'Tri'** + String get tri; + + /// No description provided for @pwm. + /// + /// In en, this message translates to: + /// **'pwm'** + String get pwm; + /// 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 6fc2a349f..ab31c0f8c 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -352,6 +352,27 @@ class AppLocalizationsEn extends AppLocalizations { @override String get produceSound => 'Produce Sound'; + @override + String get frequency => 'Frequency'; + + @override + String get phaseOffset => 'Phase Offset'; + + @override + String get unitDeg => '°'; + + @override + String get unitPercentage => '%'; + + @override + String get sine => 'Sine'; + + @override + String get tri => 'Tri'; + + @override + String get pwm => 'pwm'; + @override String get analyze => 'Analyze'; diff --git a/lib/others/wave_generator_constants.dart b/lib/others/wave_generator_constants.dart new file mode 100644 index 000000000..8a3a51aca --- /dev/null +++ b/lib/others/wave_generator_constants.dart @@ -0,0 +1,41 @@ +import 'package:pslab/providers/wave_generator_state_provider.dart'; + +class WaveGeneratorConstants { + final Map> wave = { + WaveConst.wave1: { + WaveConst.frequency: WaveData.freqMin.value, + WaveConst.waveType: WaveGeneratorStateProvider.sin, + }, + WaveConst.wave2: { + WaveConst.phase: WaveData.phaseMin.value, + WaveConst.frequency: WaveData.freqMin.value, + WaveConst.waveType: WaveGeneratorStateProvider.sin, + }, + WaveConst.waveType: {}, + WaveConst.sqr1: { + WaveConst.frequency: WaveData.freqMin.value, + WaveConst.duty: WaveData.dutyMin.value, + }, + WaveConst.sqr2: { + WaveConst.phase: WaveData.phaseMin.value, + WaveConst.duty: WaveData.dutyMin.value, + }, + WaveConst.sqr3: { + WaveConst.phase: WaveData.phaseMin.value, + WaveConst.duty: WaveData.dutyMin.value, + }, + WaveConst.sqr4: { + WaveConst.phase: WaveData.phaseMin.value, + WaveConst.duty: WaveData.dutyMin.value, + }, + }; + + WaveConst modeSelected = WaveConst.square; + + final Map state = { + 'SQR1': 0, + 'SQR2': 0, + 'SQR3': 0, + 'SQR4': 0, + }; +} diff --git a/lib/providers/wave_generator_state_provider.dart b/lib/providers/wave_generator_state_provider.dart new file mode 100644 index 000000000..ff969163c --- /dev/null +++ b/lib/providers/wave_generator_state_provider.dart @@ -0,0 +1,332 @@ +import 'dart:math'; +import 'dart:math' as math; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:pslab/communication/science_lab.dart'; +import 'package:pslab/others/wave_generator_constants.dart'; +import 'package:pslab/providers/locator.dart'; + +enum WaveConst { + waveType, + wave1, + wave2, + sqr1, + sqr2, + sqr3, + sqr4, + frequency, + phase, + duty, + sine, + triangular, + square, + pwm +} + +enum WaveData { + freqMin(10), + dutyMin(0), + phaseMin(0), + freqMax(5000), + phaseMax(360), + dutyMax(100); + + final int value; + const WaveData(this.value); + + int get getValue => value; +} + +class WaveGeneratorStateProvider extends ChangeNotifier { + static final int sin = 1; + static final int triangular = 2; + static final int pwm = 3; + + late WaveConst? selectedAnalogWave; + + late WaveConst? selectedDigitalWave; + + late WaveConst? propSelected; + + late WaveGeneratorConstants waveGeneratorConstants; + + late List> waveData; + + late ScienceLab _scienceLab; + + WaveGeneratorStateProvider() { + selectedAnalogWave = WaveConst.wave1; + + selectedDigitalWave = WaveConst.sqr1; + + _scienceLab = getIt.get(); + + propSelected = null; + + waveGeneratorConstants = WaveGeneratorConstants(); + + waveData = []; + } + + void setAnalogSelectedWave(WaveConst wave) { + selectedAnalogWave = wave; + propSelected = null; + previewWave(); + notifyListeners(); + } + + void setDigitalSelectedWave(WaveConst wave) { + selectedDigitalWave = wave; + propSelected = null; + previewWave(); + notifyListeners(); + } + + void setPropSelected(WaveConst wave) { + propSelected = wave; + previewWave(); + notifyListeners(); + } + + void setAnalogWaveType(int waveType) { + waveGeneratorConstants.wave[selectedAnalogWave]?[WaveConst.waveType] = + waveType; + previewWave(); + notifyListeners(); + } + + Future setValue(int value) async { + if (waveGeneratorConstants.modeSelected == WaveConst.square) { + waveGeneratorConstants.wave[selectedAnalogWave]?[propSelected!] = value; + } else { + if (propSelected == WaveConst.frequency) { + waveGeneratorConstants.wave[WaveConst.sqr1]?[propSelected!] = value; + } else { + waveGeneratorConstants.wave[selectedDigitalWave]?[propSelected!] = + value; + } + } + previewWave(); + await setWave(); + notifyListeners(); + } + + Future setWave() async { + double freq1 = waveGeneratorConstants + .wave[WaveConst.wave1]![WaveConst.frequency]! + .toDouble(); + double freq2 = waveGeneratorConstants + .wave[WaveConst.wave2]![WaveConst.frequency]! + .toDouble(); + double phase = waveGeneratorConstants + .wave[WaveConst.wave2]![WaveConst.phase]! + .toDouble(); + + String waveType1 = + waveGeneratorConstants.wave[WaveConst.wave1]![WaveConst.waveType]! == + sin + ? "sine" + : "tria"; + String waveType2 = + waveGeneratorConstants.wave[WaveConst.wave2]![WaveConst.waveType]! == + sin + ? "sine" + : "tria"; + + if (_scienceLab.isConnected()) { + if (waveGeneratorConstants.modeSelected == WaveConst.square) { + if (phase == WaveData.phaseMin.getValue) { + await _scienceLab.setSI1(freq1, waveType1); + await _scienceLab.setSI2(freq2, waveType2); + } else { + await _scienceLab.setWaves(freq1, phase, freq2); + } + } else { + double freqSqr1 = waveGeneratorConstants + .wave[WaveConst.sqr1]![WaveConst.frequency]! + .toDouble(); + double dutySqr1 = waveGeneratorConstants + .wave[WaveConst.sqr1]![WaveConst.duty]! + .toDouble() / + 100; + double dutySqr2 = waveGeneratorConstants + .wave[WaveConst.sqr2]![WaveConst.duty]! + .toDouble() / + 100; + double phaseSqr2 = waveGeneratorConstants + .wave[WaveConst.sqr2]![WaveConst.phase]! + .toDouble() / + 360; + double dutySqr3 = waveGeneratorConstants + .wave[WaveConst.sqr3]![WaveConst.duty]! + .toDouble() / + 100; + double phaseSqr3 = waveGeneratorConstants + .wave[WaveConst.sqr3]![WaveConst.phase]! + .toDouble() / + 360; + double dutySqr4 = waveGeneratorConstants + .wave[WaveConst.sqr4]![WaveConst.duty]! + .toDouble() / + 100; + double phaseSqr4 = waveGeneratorConstants + .wave[WaveConst.sqr4]![WaveConst.phase]! + .toDouble() / + 360; + + await _scienceLab.sqrPWM(freqSqr1, dutySqr1, phaseSqr2, dutySqr2, + phaseSqr3, dutySqr3, phaseSqr4, dutySqr4, false); + } + } + } + + Future incrementValue() async { + int min, max; + switch (propSelected) { + case WaveConst.frequency: + min = WaveData.freqMin.getValue; + max = WaveData.freqMax.getValue; + break; + case WaveConst.phase: + min = WaveData.phaseMin.getValue; + max = WaveData.phaseMax.getValue; + break; + case WaveConst.duty: + min = WaveData.dutyMin.getValue; + max = WaveData.dutyMax.getValue; + break; + default: + return; + } + + int current = waveGeneratorConstants.modeSelected == WaveConst.square + ? (waveGeneratorConstants.wave[selectedAnalogWave]?[propSelected!] ?? + min) + : propSelected == WaveConst.frequency + ? (waveGeneratorConstants.wave[WaveConst.sqr1]?[propSelected!] ?? + min) + : (waveGeneratorConstants.wave[selectedDigitalWave] + ?[propSelected!] ?? + min); + + if (current < max) await setValue(current + 1); + } + + Future decrementValue() async { + int min; + switch (propSelected) { + case WaveConst.frequency: + min = WaveData.freqMin.getValue; + break; + case WaveConst.phase: + min = WaveData.phaseMin.getValue; + break; + case WaveConst.duty: + min = WaveData.dutyMin.getValue; + break; + default: + return; + } + + int current = waveGeneratorConstants.modeSelected == WaveConst.square + ? (waveGeneratorConstants.wave[selectedAnalogWave]?[propSelected!] ?? + min) + : propSelected == WaveConst.frequency + ? (waveGeneratorConstants.wave[WaveConst.sqr1]?[propSelected!] ?? + min) + : (waveGeneratorConstants.wave[selectedDigitalWave] + ?[propSelected!] ?? + min); + + if (current > min) await setValue(current - 1); + } + + void previewWave() { + waveData.clear(); + List samplePoints = getSamplePoints(false); + List referencePoints = getSamplePoints(true); + waveData.add(referencePoints); + waveData.add(samplePoints); + notifyListeners(); + } + + List getSamplePoints(bool isReference) { + List entries = []; + if (waveGeneratorConstants.modeSelected == WaveConst.pwm) { + double freq = waveGeneratorConstants + .wave[WaveConst.sqr1]![WaveConst.frequency]! + .toDouble(); + double duty = waveGeneratorConstants + .wave[selectedDigitalWave]![WaveConst.duty]! + .toDouble() / + 100; + double phase = 0; + if (selectedDigitalWave != WaveConst.sqr1 && !isReference) { + phase = waveGeneratorConstants.wave[selectedDigitalWave] + ?[WaveConst.phase]! + .toDouble() ?? + 0; + } + for (int i = 0; i < 5000; i++) { + double t = 2 * pi * freq * i / 1e6 + phase * pi / 180; + double y; + if (t % (2 * pi) < 2 * pi * duty) { + y = 5; + } else { + y = -5; + } + entries.add(FlSpot(i.toDouble(), y)); + } + } else { + double phase = 0; + int shape = + waveGeneratorConstants.wave[selectedAnalogWave]![WaveConst.waveType]!; + + double freq = waveGeneratorConstants + .wave[selectedAnalogWave]![WaveConst.frequency]! + .toDouble(); + + if (selectedAnalogWave != WaveConst.wave1 && !isReference) { + phase = waveGeneratorConstants.wave[WaveConst.wave2]![WaveConst.phase]! + .toDouble(); + } + if (shape == 1) { + for (int i = 0; i < 5000; i++) { + double y = 5 * math.sin(2 * pi * (freq / 1e6) * i + phase * pi / 180); + entries.add(FlSpot(i.toDouble(), y)); + } + } else { + for (int i = 0; i < 5000; i++) { + double y = (10 / pi) * + (math.asin( + math.sin(2 * pi * (freq / 1e6) * i + phase * pi / 180))); + entries.add(FlSpot(i.toDouble(), y)); + } + } + } + return entries; + } + + List createPlots() { + List colors = [Colors.white, Colors.white60]; + List plots = []; + plots.addAll( + List.generate( + waveData.length, + (index) { + return LineChartBarData( + spots: waveData[index], + isCurved: false, + color: colors[index % colors.length], + barWidth: 1, + dotData: const FlDotData( + show: false, + ), + ); + }, + ), + ); + return plots; + } +} diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index b0f3db96c..61c280ee2 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -84,3 +84,6 @@ Color sensorControlsTextBox = Colors.grey.shade400; Color sensorControlIconColor = Colors.grey.shade600; List bmp180ChartColors = [Colors.blue, Colors.green, Colors.red]; Color chartHintTextColor = Colors.yellow; +Color buttonEnabledColor = primaryRed; +Color buttonDisabledColor = Color.fromARGB(255, 240, 162, 162); +Color waveGeneratorPropTextColor = Colors.deepOrange; diff --git a/lib/view/wave_generator_screen.dart b/lib/view/wave_generator_screen.dart index eaa72f7f9..de843c92b 100644 --- a/lib/view/wave_generator_screen.dart +++ b/lib/view/wave_generator_screen.dart @@ -1,9 +1,12 @@ 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/wave_generator_state_provider.dart'; import 'package:pslab/theme/colors.dart'; import 'package:pslab/view/widgets/common_scaffold_widget.dart'; import 'package:pslab/view/widgets/analog_waveform_controls.dart'; +import 'package:pslab/view/widgets/digital_waveform_controls.dart'; import 'package:pslab/view/widgets/wave_generator_graph.dart'; import 'package:pslab/view/widgets/wave_generator_main_controls.dart'; @@ -18,100 +21,138 @@ class _WaveGeneratorScreenState extends State { AppLocalizations appLocalizations = getIt.get(); @override Widget build(BuildContext context) { - return CommonScaffold( - title: 'Wave Generator', - body: Container( - margin: const EdgeInsets.all(8.0), - child: Column( - children: [ - Expanded( - flex: 30, - child: Container( - color: chartBackgroundColor, - child: WaveGeneratorGraph(), - ), - ), - Expanded( - flex: 30, - child: Column( - children: [ - Expanded( - flex: 70, - child: AnalogWaveformControls(), - ), - Expanded( - flex: 30, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, + return SafeArea( + child: MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => WaveGeneratorStateProvider(), + ), + ], + child: Consumer( + builder: (context, provider, _) { + return CommonScaffold( + title: appLocalizations.waveGenerator, + body: Container( + margin: const EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0), + child: Column( + children: [ + Expanded( + flex: 30, + child: Container( + color: chartBackgroundColor, + child: WaveGeneratorGraph(), + ), + ), + Column( children: [ - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: primaryRed, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - child: Text( - appLocalizations.produceSound, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - onPressed: () => {}, - ), - ), - const SizedBox(width: 4), - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: primaryRed, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), + provider.waveGeneratorConstants.modeSelected == + WaveConst.square + ? AnalogWaveformControls() + : DigitalWaveformControls(), + SizedBox( + height: 60, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: primaryRed, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: Text( + appLocalizations.produceSound, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + onPressed: () => {}, + ), ), - ), - child: Text( - appLocalizations.analog, - style: TextStyle( - color: Colors.white, - fontSize: 14, + const SizedBox(width: 4), + Expanded( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: provider + .waveGeneratorConstants + .modeSelected == + WaveConst.square + ? buttonEnabledColor + : buttonDisabledColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: Text( + appLocalizations.analog, + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + onPressed: () => { + setState( + () { + provider.waveGeneratorConstants + .modeSelected = WaveConst.square; + provider.propSelected = null; + provider.previewWave(); + }, + ), + }, + ), ), - ), - onPressed: () => {}, - ), - ), - const SizedBox(width: 4), - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: primaryRed, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), + const SizedBox(width: 4), + Expanded( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: provider + .waveGeneratorConstants + .modeSelected == + WaveConst.pwm + ? buttonEnabledColor + : buttonDisabledColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: Text( + appLocalizations.digital, + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + onPressed: () => { + setState( + () { + provider.waveGeneratorConstants + .modeSelected = WaveConst.pwm; + provider.propSelected = null; + provider.previewWave(); + }, + ), + }, + ), ), - ), - child: Text( - appLocalizations.digital, - style: TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - onPressed: () => {}, + ], ), ), ], ), - ), - ], + Expanded( + flex: 40, + child: WaveGeneratorMainControls(), + ), + ], + ), ), - ), - Expanded( - flex: 40, - child: WaveGeneratorMainControls(), - ), - ], + ); + }, ), ), ); diff --git a/lib/view/widgets/analog_waveform_controls.dart b/lib/view/widgets/analog_waveform_controls.dart index 629c1b543..f5e3deca2 100644 --- a/lib/view/widgets/analog_waveform_controls.dart +++ b/lib/view/widgets/analog_waveform_controls.dart @@ -1,6 +1,8 @@ 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/wave_generator_state_provider.dart'; import 'package:pslab/theme/colors.dart'; class AnalogWaveformControls extends StatefulWidget { @@ -16,6 +18,8 @@ class _AnalogWaveformControlsState extends State { String iconTriangular = "assets/icons/ic_triangular.png"; @override Widget build(BuildContext context) { + WaveGeneratorStateProvider waveGeneratorStateProvider = + Provider.of(context); return Stack( children: [ Container( @@ -27,96 +31,156 @@ class _AnalogWaveformControlsState extends State { ), child: Column( children: [ - Row( - children: [ - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: primaryRed, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - child: Text( - appLocalizations.wave1, - style: TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - onPressed: () => {}, - ), - ), - const SizedBox(width: 4), - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: primaryRed, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - child: Text( - appLocalizations.wave2, - style: TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - onPressed: () => {}, - ), - ), - ], - ), - IntrinsicHeight( + SizedBox( + height: 40, child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( - flex: 35, child: TextButton( style: TextButton.styleFrom( - backgroundColor: primaryRed, + backgroundColor: + waveGeneratorStateProvider.selectedAnalogWave == + WaveConst.wave1 + ? buttonEnabledColor + : buttonDisabledColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), ), child: Text( - appLocalizations.freq, + appLocalizations.wave1, style: TextStyle( color: Colors.white, fontSize: 14, ), ), - onPressed: () => {}, + onPressed: () => { + setState( + () { + waveGeneratorStateProvider + .setAnalogSelectedWave(WaveConst.wave1); + }, + ), + }, ), ), const SizedBox(width: 4), + Expanded( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: + waveGeneratorStateProvider.selectedAnalogWave == + WaveConst.wave2 + ? buttonEnabledColor + : buttonDisabledColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: Text( + appLocalizations.wave2, + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + onPressed: () => { + setState( + () { + waveGeneratorStateProvider + .setAnalogSelectedWave(WaveConst.wave2); + }, + ), + }, + ), + ), + ], + ), + ), + const SizedBox(height: 4), + SizedBox( + height: 40, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ Expanded( flex: 35, child: TextButton( style: TextButton.styleFrom( - backgroundColor: primaryRed, + backgroundColor: + waveGeneratorStateProvider.propSelected == + WaveConst.frequency + ? buttonEnabledColor + : buttonDisabledColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), ), child: Text( - appLocalizations.phase, + appLocalizations.freq, style: TextStyle( color: Colors.white, fontSize: 14, ), ), - onPressed: () => {}, + onPressed: () => { + setState( + () { + waveGeneratorStateProvider + .setPropSelected(WaveConst.frequency); + }, + ), + }, ), ), const SizedBox(width: 4), + Expanded( + flex: 35, + child: waveGeneratorStateProvider.selectedAnalogWave == + WaveConst.wave2 + ? TextButton( + style: TextButton.styleFrom( + backgroundColor: + waveGeneratorStateProvider.propSelected == + WaveConst.phase + ? buttonEnabledColor + : buttonDisabledColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: Text( + appLocalizations.phase, + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + onPressed: () => { + setState( + () { + waveGeneratorStateProvider + .setPropSelected(WaveConst.phase); + }, + ), + }, + ) + : Container(), + ), + const SizedBox(width: 4), Expanded( flex: 15, child: IconButton( style: TextButton.styleFrom( - backgroundColor: primaryRed, + backgroundColor: waveGeneratorStateProvider + .waveGeneratorConstants.wave[ + waveGeneratorStateProvider + .selectedAnalogWave] + ?[WaveConst.waveType] == + WaveGeneratorStateProvider.sin + ? buttonEnabledColor + : buttonDisabledColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), @@ -125,7 +189,12 @@ class _AnalogWaveformControlsState extends State { iconSin, color: Colors.white, ), - onPressed: () => {}, + onPressed: () => { + setState( + () => waveGeneratorStateProvider.setAnalogWaveType( + WaveGeneratorStateProvider.sin), + ), + }, ), ), const SizedBox(width: 4), @@ -133,7 +202,14 @@ class _AnalogWaveformControlsState extends State { flex: 15, child: IconButton( style: TextButton.styleFrom( - backgroundColor: primaryRed, + backgroundColor: waveGeneratorStateProvider + .waveGeneratorConstants.wave[ + waveGeneratorStateProvider + .selectedAnalogWave] + ?[WaveConst.waveType] == + WaveGeneratorStateProvider.triangular + ? buttonEnabledColor + : buttonDisabledColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), @@ -142,7 +218,12 @@ class _AnalogWaveformControlsState extends State { iconTriangular, color: Colors.white, ), - onPressed: () => {}, + onPressed: () => { + setState( + () => waveGeneratorStateProvider.setAnalogWaveType( + WaveGeneratorStateProvider.triangular), + ), + }, ), ), ], diff --git a/lib/view/widgets/digital_waveform_controls.dart b/lib/view/widgets/digital_waveform_controls.dart index e16cb77a7..ee6950cc4 100644 --- a/lib/view/widgets/digital_waveform_controls.dart +++ b/lib/view/widgets/digital_waveform_controls.dart @@ -1,6 +1,8 @@ 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/wave_generator_state_provider.dart'; import 'package:pslab/theme/colors.dart'; class DigitalWaveformControls extends StatefulWidget { @@ -14,6 +16,8 @@ class _DigitalWaveformControlsState extends State { AppLocalizations appLocalizations = getIt.get(); @override Widget build(BuildContext context) { + WaveGeneratorStateProvider waveGeneratorStateProvider = + Provider.of(context); return Stack( children: [ Container( @@ -25,134 +29,201 @@ class _DigitalWaveformControlsState extends State { ), child: Column( children: [ - Row( - children: [ - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: primaryRed, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - child: Text( - appLocalizations.sqr1.toUpperCase(), - style: TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - onPressed: () => {}, - ), - ), - const SizedBox(width: 4), - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: primaryRed, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), + SizedBox( + height: 40, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: + waveGeneratorStateProvider.selectedDigitalWave == + WaveConst.sqr1 + ? buttonEnabledColor + : buttonDisabledColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), ), - ), - child: Text( - appLocalizations.sqr2.toUpperCase(), - style: TextStyle( - color: Colors.white, - fontSize: 14, + child: Text( + appLocalizations.sqr1.toUpperCase(), + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), ), + onPressed: () => { + setState(() { + waveGeneratorStateProvider + .setDigitalSelectedWave(WaveConst.sqr1); + }) + }, ), - onPressed: () => {}, ), - ), - const SizedBox(width: 4), - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: primaryRed, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), + const SizedBox(width: 4), + Expanded( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: + waveGeneratorStateProvider.selectedDigitalWave == + WaveConst.sqr2 + ? buttonEnabledColor + : buttonDisabledColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), ), - ), - child: Text( - appLocalizations.sqr3.toUpperCase(), - style: TextStyle( - color: Colors.white, - fontSize: 14, + child: Text( + appLocalizations.sqr2.toUpperCase(), + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), ), + onPressed: () => { + setState(() { + waveGeneratorStateProvider + .setDigitalSelectedWave(WaveConst.sqr2); + }) + }, ), - onPressed: () => {}, ), - ), - const SizedBox(width: 4), - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: primaryRed, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), + const SizedBox(width: 4), + Expanded( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: + waveGeneratorStateProvider.selectedDigitalWave == + WaveConst.sqr3 + ? buttonEnabledColor + : buttonDisabledColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), ), - ), - child: Text( - appLocalizations.sqr4.toUpperCase(), - style: TextStyle( - color: Colors.white, - fontSize: 14, + child: Text( + appLocalizations.sqr3.toUpperCase(), + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), ), + onPressed: () => { + setState(() { + waveGeneratorStateProvider + .setDigitalSelectedWave(WaveConst.sqr3); + }) + }, ), - onPressed: () => {}, ), - ), - ], - ), - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + const SizedBox(width: 4), Expanded( - flex: 35, child: TextButton( style: TextButton.styleFrom( - backgroundColor: primaryRed, + backgroundColor: + waveGeneratorStateProvider.selectedDigitalWave == + WaveConst.sqr4 + ? buttonEnabledColor + : buttonDisabledColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), ), child: Text( - appLocalizations.freq, + appLocalizations.sqr4.toUpperCase(), style: TextStyle( color: Colors.white, fontSize: 14, ), ), - onPressed: () => {}, + onPressed: () => { + setState(() { + waveGeneratorStateProvider + .setDigitalSelectedWave(WaveConst.sqr4); + }) + }, ), ), - const SizedBox(width: 4), + ], + ), + ), + const SizedBox(height: 4), + SizedBox( + height: 40, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ Expanded( flex: 35, child: TextButton( style: TextButton.styleFrom( - backgroundColor: primaryRed, + backgroundColor: + waveGeneratorStateProvider.propSelected == + WaveConst.frequency + ? buttonEnabledColor + : buttonDisabledColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), ), child: Text( - appLocalizations.phase, + appLocalizations.freq, style: TextStyle( color: Colors.white, fontSize: 14, ), ), - onPressed: () => {}, + onPressed: () => { + setState(() { + waveGeneratorStateProvider + .setPropSelected(WaveConst.frequency); + }) + }, ), ), const SizedBox(width: 4), + Expanded( + flex: 35, + child: waveGeneratorStateProvider.selectedDigitalWave != + WaveConst.sqr1 + ? TextButton( + style: TextButton.styleFrom( + backgroundColor: + waveGeneratorStateProvider.propSelected == + WaveConst.phase + ? buttonEnabledColor + : buttonDisabledColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: Text( + appLocalizations.phase, + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + onPressed: () => { + setState(() { + waveGeneratorStateProvider + .setPropSelected(WaveConst.phase); + }) + }, + ) + : Container(), + ), + const SizedBox(width: 4), Expanded( flex: 35, child: TextButton( style: TextButton.styleFrom( - backgroundColor: primaryRed, + backgroundColor: + waveGeneratorStateProvider.propSelected == + WaveConst.duty + ? buttonEnabledColor + : buttonDisabledColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), @@ -164,7 +235,12 @@ class _DigitalWaveformControlsState extends State { fontSize: 14, ), ), - onPressed: () => {}, + onPressed: () => { + setState(() { + waveGeneratorStateProvider + .setPropSelected(WaveConst.duty); + }) + }, ), ), ], diff --git a/lib/view/widgets/wave_generator_graph.dart b/lib/view/widgets/wave_generator_graph.dart index 3f8672416..628056226 100644 --- a/lib/view/widgets/wave_generator_graph.dart +++ b/lib/view/widgets/wave_generator_graph.dart @@ -1,5 +1,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/providers/wave_generator_state_provider.dart'; import 'package:pslab/theme/colors.dart'; class WaveGeneratorGraph extends StatefulWidget { @@ -26,9 +28,12 @@ class _WaveGeneratorGraphState extends State { @override Widget build(BuildContext context) { + WaveGeneratorStateProvider waveGeneratorStateProvider = + Provider.of(context, listen: true); return Container( padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20), child: LineChart( + duration: const Duration(milliseconds: 10), LineChartData( backgroundColor: chartBackgroundColor, titlesData: FlTitlesData( @@ -81,10 +86,11 @@ class _WaveGeneratorGraphState extends State { ), ), clipData: const FlClipData.all(), - maxY: 5.0, - minY: -5.0, + maxY: 7.0, + minY: -7.0, maxX: 5000.0, minX: 0.0, + lineBarsData: waveGeneratorStateProvider.createPlots(), ), ), ); diff --git a/lib/view/widgets/wave_generator_main_controls.dart b/lib/view/widgets/wave_generator_main_controls.dart index f3f3179f3..2b42ba2ca 100644 --- a/lib/view/widgets/wave_generator_main_controls.dart +++ b/lib/view/widgets/wave_generator_main_controls.dart @@ -1,4 +1,8 @@ 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/wave_generator_state_provider.dart'; import 'package:pslab/theme/colors.dart'; class WaveGeneratorMainControls extends StatefulWidget { @@ -9,113 +13,196 @@ class WaveGeneratorMainControls extends StatefulWidget { } class _WaveGeneratorMainControlsState extends State { + AppLocalizations appLocalizations = getIt.get(); String iconSin = "assets/icons/ic_sin.png"; String iconTriangular = "assets/icons/ic_triangular.png"; + String iconPwm = "assets/icons/ic_pwm_pic.png"; + var labelMap = {}; + var unitMap = {}; + final minValues = { + WaveConst.frequency: WaveData.freqMin.value, + WaveConst.phase: WaveData.phaseMin.value, + WaveConst.duty: WaveData.dutyMin.value, + }; + final maxValues = { + WaveConst.frequency: WaveData.freqMax.value, + WaveConst.phase: WaveData.phaseMax.value, + WaveConst.duty: WaveData.dutyMax.value, + }; @override Widget build(BuildContext context) { - return Column( - children: [ - Expanded( - flex: 75, - child: Container( - margin: const EdgeInsets.only(top: 4), - color: Colors.black, - child: Column( - children: [ - Expanded( - flex: 80, - child: IntrinsicHeight( - child: Row( - children: [ - Expanded( - flex: 20, - child: Container( - margin: const EdgeInsets.only(left: 8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.asset( - iconSin, - height: 40, - width: 40, - ), - Text( - 'Sine', - style: TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.bold, - ), + labelMap = { + WaveConst.frequency: appLocalizations.frequency, + WaveConst.phase: appLocalizations.phaseOffset, + WaveConst.duty: appLocalizations.duty, + }; + unitMap = { + WaveConst.frequency: appLocalizations.unitHz, + WaveConst.phase: appLocalizations.unitDeg, + WaveConst.duty: appLocalizations.unitPercentage, + }; + return Consumer( + builder: (context, provider, _) { + return Column( + children: [ + Expanded( + flex: 75, + child: Container( + margin: const EdgeInsets.only(top: 4), + color: Colors.black, + child: Column( + children: [ + Expanded( + flex: 80, + child: IntrinsicHeight( + child: Row( + children: [ + Expanded( + flex: 20, + child: Container( + margin: const EdgeInsets.only(left: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + provider.waveGeneratorConstants + .modeSelected == + WaveConst.square + ? (provider.waveGeneratorConstants + .wave[ + provider + .selectedAnalogWave] + ?[WaveConst.waveType] == + WaveGeneratorStateProvider.sin + ? iconSin + : iconTriangular) + : iconPwm, + height: 40, + width: 40, + ), + Text( + provider.waveGeneratorConstants + .modeSelected == + WaveConst.square + ? (provider.waveGeneratorConstants + .wave[ + provider + .selectedAnalogWave] + ?[WaveConst.waveType] == + WaveGeneratorStateProvider.sin + ? appLocalizations.sine + : appLocalizations.tri) + : appLocalizations.pwm.toUpperCase(), + style: TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + ], ), - ], + ), ), - ), - ), - const VerticalDivider(), - Expanded( - flex: 80, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Frequency:', - style: TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.bold, + const VerticalDivider(), + Expanded( + flex: 80, + child: Container( + margin: EdgeInsets.only( + right: 8, ), - ), - Text( - 'Phase:', - style: TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.bold, + child: Column( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + provider.waveGeneratorConstants + .modeSelected == + WaveConst.square + ? '${appLocalizations.frequency}: ${provider.waveGeneratorConstants.wave[provider.selectedAnalogWave]?[WaveConst.frequency]} Hz' + : '${appLocalizations.frequency}: ${provider.waveGeneratorConstants.wave[WaveConst.sqr1]?[WaveConst.frequency]} Hz', + style: TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + provider.waveGeneratorConstants + .modeSelected == + WaveConst.square + ? '${appLocalizations.phase}: ${provider.waveGeneratorConstants.wave[provider.selectedAnalogWave]?[WaveConst.phase] ?? '--'}°' + : '${appLocalizations.phase}: ${provider.waveGeneratorConstants.wave[provider.selectedDigitalWave]?[WaveConst.phase] ?? '--'}°', + style: TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + provider.waveGeneratorConstants + .modeSelected == + WaveConst.pwm + ? Text( + '${appLocalizations.duty}: ${provider.waveGeneratorConstants.wave[provider.selectedDigitalWave]?[WaveConst.duty]}%', + style: TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ) + : Container(), + ], + ), + ], ), ), - ], - ), + ), + ], ), - ], - ), - ), - ), - Transform.translate( - offset: const Offset(0, -8), - child: const Divider(), - ), - Expanded( - flex: 20, - child: Transform.translate( - offset: const Offset(0, -8), - child: Container( - margin: const EdgeInsets.only( - left: 32, ), - alignment: Alignment.centerLeft, - child: Text( - 'Phase Offset:', - style: TextStyle( - color: Colors.deepOrange, - fontSize: 16, + ), + Transform.translate( + offset: const Offset(0, -8), + child: const Divider(), + ), + Expanded( + flex: 20, + child: Transform.translate( + offset: const Offset(0, -8), + child: Container( + margin: const EdgeInsets.only( + left: 32, + ), + alignment: Alignment.centerLeft, + child: provider.propSelected != null + ? Text( + provider.waveGeneratorConstants + .modeSelected == + WaveConst.square + ? '${labelMap[provider.propSelected]}: ${provider.waveGeneratorConstants.wave[provider.selectedAnalogWave]?[provider.propSelected]}${unitMap[provider.propSelected]}' + : (provider.propSelected == + WaveConst.frequency + ? '${labelMap[provider.propSelected]}: ${provider.waveGeneratorConstants.wave[WaveConst.sqr1]?[provider.propSelected]}${unitMap[provider.propSelected]}' + : '${labelMap[provider.propSelected]}: ${provider.waveGeneratorConstants.wave[provider.selectedDigitalWave]?[provider.propSelected]}${unitMap[provider.propSelected]}'), + style: TextStyle( + color: waveGeneratorPropTextColor, + fontSize: 16, + ), + ) + : Container(), ), ), ), - ), + ], ), - ], - ), - ), - ), - Expanded( - flex: 25, - child: Container( - margin: const EdgeInsets.only( - bottom: 16, + ), ), - child: Row( + Row( children: [ SizedBox( height: 35, @@ -123,9 +210,15 @@ class _WaveGeneratorMainControlsState extends State { child: IconButton.filled( padding: EdgeInsets.zero, icon: Icon(Icons.chevron_left), - onPressed: () async {}, + onPressed: () async { + if (provider.propSelected != null) { + await provider.decrementValue(); + } + }, style: IconButton.styleFrom( - backgroundColor: primaryRed, + backgroundColor: provider.propSelected == null + ? buttonDisabledColor + : buttonEnabledColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(4), ), @@ -141,11 +234,39 @@ class _WaveGeneratorMainControlsState extends State { const RoundSliderThumbShape(enabledThumbRadius: 6), ), child: Slider( - activeColor: primaryRed, - min: 0, - max: 5000, - value: 0, - onChanged: (value) {}, + activeColor: provider.propSelected == null + ? buttonDisabledColor + : sliderActiveColor, + min: minValues[provider.propSelected]?.toDouble() ?? + WaveData.freqMin.value.toDouble(), + max: maxValues[provider.propSelected]?.toDouble() ?? + WaveData.freqMax.value.toDouble(), + value: provider.waveGeneratorConstants.modeSelected == + WaveConst.square + ? provider + .waveGeneratorConstants + .wave[provider.selectedAnalogWave]![ + provider.propSelected] + ?.toDouble() ?? + WaveData.freqMin.value.toDouble() + : (provider.propSelected == WaveConst.frequency + ? provider + .waveGeneratorConstants + .wave[WaveConst.sqr1]![ + provider.propSelected] + ?.toDouble() ?? + WaveData.freqMin.value.toDouble() + : provider + .waveGeneratorConstants + .wave[provider.selectedDigitalWave]![ + provider.propSelected] + ?.toDouble() ?? + WaveData.freqMin.value.toDouble()), + onChanged: (value) async { + if (provider.propSelected != null) { + await provider.setValue(value.round()); + } + }, ), ), ), @@ -155,9 +276,15 @@ class _WaveGeneratorMainControlsState extends State { child: IconButton.filled( padding: EdgeInsets.zero, icon: Icon(Icons.chevron_right), - onPressed: () async {}, + onPressed: () async { + if (provider.propSelected != null) { + await provider.incrementValue(); + } + }, style: IconButton.styleFrom( - backgroundColor: primaryRed, + backgroundColor: provider.propSelected == null + ? buttonDisabledColor + : buttonEnabledColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(4), ), @@ -166,9 +293,9 @@ class _WaveGeneratorMainControlsState extends State { ), ], ), - ), - ), - ], + ], + ); + }, ); } } From a511e8be15cb11fc986ac65495c5fa7d54aed7a6 Mon Sep 17 00:00:00 2001 From: Anashuman Singh Date: Sat, 16 Aug 2025 12:48:42 +0530 Subject: [PATCH 2/2] feat: added guide screen --- assets/images/sin_wave_circuit.png | Bin 0 -> 39416 bytes assets/images/square_wave_circuit.png | Bin 0 -> 5236 bytes lib/l10n/app_en.arb | 18 ++ lib/l10n/app_localizations.dart | 108 ++++++++++ lib/l10n/app_localizations_en.dart | 65 ++++++ lib/view/wave_generator_screen.dart | 298 +++++++++++++++++--------- 6 files changed, 383 insertions(+), 106 deletions(-) create mode 100644 assets/images/sin_wave_circuit.png create mode 100644 assets/images/square_wave_circuit.png diff --git a/assets/images/sin_wave_circuit.png b/assets/images/sin_wave_circuit.png new file mode 100644 index 0000000000000000000000000000000000000000..b54ecc23752706a87cd4545049c8d333969cf500 GIT binary patch literal 39416 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 literal 0 HcmV?d00001 diff --git a/assets/images/square_wave_circuit.png b/assets/images/square_wave_circuit.png new file mode 100644 index 0000000000000000000000000000000000000000..90ffc67fccf5f8f05ebe90e14bef6f8ed85c9791 GIT binary patch literal 5236 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 literal 0 HcmV?d00001 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d8ea17397..81b233d6b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -118,6 +118,24 @@ "sine": "Sine", "tri": "Tri", "pwm": "pwm", + "waveGeneratorIntro": "The wave generator can be used to generate different types of waves like Sine wave, square wave and saw-tooth wave allow us to change their characteristics like frequency, phase and duty. It also allows us to produce PWM signals having different phase and duty.", + "sineWaveCaption": "To generate Sine wave or Saw-Tooth wave:", + "sineWaveBulletPoint1": "Connect the Wave pins S1 and S2 to the channel pins CH1, CH2 as shown in the above figure.", + "sineWaveBulletPoint2": "Select the Wave1 button for S1 pin and Wave2 button for S2 pin.", + "sineWaveBulletPoint3": "Press Sine image button for Sine wave and Saw-Tooth image button for Saw-Tooth wave.", + "sineWaveBulletPoint4": "Set their respective frequencies and phase difference(optional) using buttons in waveform panel.", + "sineWaveBulletPoint5": "Press the View button to view the waves in oscilloscope.", + "squareWaveCaption": "To generate Square wave:", + "squareWaveBulletPoint1": "Connect the Wave pins SQ1 to the channel pin CH1 as shown in the above figure.", + "squareWaveBulletPoint2": "Ensure the mode is selected to the Square, if not press the mode button to switch to Square mode.", + "squareWaveBulletPoint3": "Select the SQ1 button", + "squareWaveBulletPoint4": "Set its Frequency and Duty Cycle", + "squareWaveBulletPoint5": "Press the View button to view the square wave in oscilloscope.", + "pwmCaption": "Similarly, to produce four different PWM signals:", + "pwmBulletPoint1": "Switch over to PWM mode(In this mode S1 and S2 pin will be disabled).", + "pwmBulletPoint2": "Set the common frequency for all the SQ pins.", + "pwmBulletPoint3": "Set the duty and phase for all the SQ pins.", + "pwmBulletPoint4": "Press View button to generate the PWM signals.", "analyze": "Analyze", "settings": "Settings", "autoStart": "Auto Start", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index de9becc01..b4dbb77d3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -802,6 +802,114 @@ abstract class AppLocalizations { /// **'pwm'** String get pwm; + /// No description provided for @waveGeneratorIntro. + /// + /// In en, this message translates to: + /// **'The wave generator can be used to generate different types of waves like Sine wave, square wave and saw-tooth wave allow us to change their characteristics like frequency, phase and duty. It also allows us to produce PWM signals having different phase and duty.'** + String get waveGeneratorIntro; + + /// No description provided for @sineWaveCaption. + /// + /// In en, this message translates to: + /// **'To generate Sine wave or Saw-Tooth wave:'** + String get sineWaveCaption; + + /// No description provided for @sineWaveBulletPoint1. + /// + /// In en, this message translates to: + /// **'Connect the Wave pins S1 and S2 to the channel pins CH1, CH2 as shown in the above figure.'** + String get sineWaveBulletPoint1; + + /// No description provided for @sineWaveBulletPoint2. + /// + /// In en, this message translates to: + /// **'Select the Wave1 button for S1 pin and Wave2 button for S2 pin.'** + String get sineWaveBulletPoint2; + + /// No description provided for @sineWaveBulletPoint3. + /// + /// In en, this message translates to: + /// **'Press Sine image button for Sine wave and Saw-Tooth image button for Saw-Tooth wave.'** + String get sineWaveBulletPoint3; + + /// No description provided for @sineWaveBulletPoint4. + /// + /// In en, this message translates to: + /// **'Set their respective frequencies and phase difference(optional) using buttons in waveform panel.'** + String get sineWaveBulletPoint4; + + /// No description provided for @sineWaveBulletPoint5. + /// + /// In en, this message translates to: + /// **'Press the View button to view the waves in oscilloscope.'** + String get sineWaveBulletPoint5; + + /// No description provided for @squareWaveCaption. + /// + /// In en, this message translates to: + /// **'To generate Square wave:'** + String get squareWaveCaption; + + /// No description provided for @squareWaveBulletPoint1. + /// + /// In en, this message translates to: + /// **'Connect the Wave pins SQ1 to the channel pin CH1 as shown in the above figure.'** + String get squareWaveBulletPoint1; + + /// No description provided for @squareWaveBulletPoint2. + /// + /// In en, this message translates to: + /// **'Ensure the mode is selected to the Square, if not press the mode button to switch to Square mode.'** + String get squareWaveBulletPoint2; + + /// No description provided for @squareWaveBulletPoint3. + /// + /// In en, this message translates to: + /// **'Select the SQ1 button'** + String get squareWaveBulletPoint3; + + /// No description provided for @squareWaveBulletPoint4. + /// + /// In en, this message translates to: + /// **'Set its Frequency and Duty Cycle'** + String get squareWaveBulletPoint4; + + /// No description provided for @squareWaveBulletPoint5. + /// + /// In en, this message translates to: + /// **'Press the View button to view the square wave in oscilloscope.'** + String get squareWaveBulletPoint5; + + /// No description provided for @pwmCaption. + /// + /// In en, this message translates to: + /// **'Similarly, to produce four different PWM signals:'** + String get pwmCaption; + + /// No description provided for @pwmBulletPoint1. + /// + /// In en, this message translates to: + /// **'Switch over to PWM mode(In this mode S1 and S2 pin will be disabled).'** + String get pwmBulletPoint1; + + /// No description provided for @pwmBulletPoint2. + /// + /// In en, this message translates to: + /// **'Set the common frequency for all the SQ pins.'** + String get pwmBulletPoint2; + + /// No description provided for @pwmBulletPoint3. + /// + /// In en, this message translates to: + /// **'Set the duty and phase for all the SQ pins.'** + String get pwmBulletPoint3; + + /// No description provided for @pwmBulletPoint4. + /// + /// In en, this message translates to: + /// **'Press View button to generate the PWM signals.'** + String get pwmBulletPoint4; + /// 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 ab31c0f8c..6c8d6cbd8 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -373,6 +373,71 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pwm => 'pwm'; + @override + String get waveGeneratorIntro => + 'The wave generator can be used to generate different types of waves like Sine wave, square wave and saw-tooth wave allow us to change their characteristics like frequency, phase and duty. It also allows us to produce PWM signals having different phase and duty.'; + + @override + String get sineWaveCaption => 'To generate Sine wave or Saw-Tooth wave:'; + + @override + String get sineWaveBulletPoint1 => + 'Connect the Wave pins S1 and S2 to the channel pins CH1, CH2 as shown in the above figure.'; + + @override + String get sineWaveBulletPoint2 => + 'Select the Wave1 button for S1 pin and Wave2 button for S2 pin.'; + + @override + String get sineWaveBulletPoint3 => + 'Press Sine image button for Sine wave and Saw-Tooth image button for Saw-Tooth wave.'; + + @override + String get sineWaveBulletPoint4 => + 'Set their respective frequencies and phase difference(optional) using buttons in waveform panel.'; + + @override + String get sineWaveBulletPoint5 => + 'Press the View button to view the waves in oscilloscope.'; + + @override + String get squareWaveCaption => 'To generate Square wave:'; + + @override + String get squareWaveBulletPoint1 => + 'Connect the Wave pins SQ1 to the channel pin CH1 as shown in the above figure.'; + + @override + String get squareWaveBulletPoint2 => + 'Ensure the mode is selected to the Square, if not press the mode button to switch to Square mode.'; + + @override + String get squareWaveBulletPoint3 => 'Select the SQ1 button'; + + @override + String get squareWaveBulletPoint4 => 'Set its Frequency and Duty Cycle'; + + @override + String get squareWaveBulletPoint5 => + 'Press the View button to view the square wave in oscilloscope.'; + + @override + String get pwmCaption => 'Similarly, to produce four different PWM signals:'; + + @override + String get pwmBulletPoint1 => + 'Switch over to PWM mode(In this mode S1 and S2 pin will be disabled).'; + + @override + String get pwmBulletPoint2 => 'Set the common frequency for all the SQ pins.'; + + @override + String get pwmBulletPoint3 => 'Set the duty and phase for all the SQ pins.'; + + @override + String get pwmBulletPoint4 => + 'Press View button to generate the PWM signals.'; + @override String get analyze => 'Analyze'; diff --git a/lib/view/wave_generator_screen.dart b/lib/view/wave_generator_screen.dart index de843c92b..b586d3a44 100644 --- a/lib/view/wave_generator_screen.dart +++ b/lib/view/wave_generator_screen.dart @@ -7,10 +7,13 @@ import 'package:pslab/theme/colors.dart'; import 'package:pslab/view/widgets/common_scaffold_widget.dart'; import 'package:pslab/view/widgets/analog_waveform_controls.dart'; import 'package:pslab/view/widgets/digital_waveform_controls.dart'; +import 'package:pslab/view/widgets/guide_widget.dart'; import 'package:pslab/view/widgets/wave_generator_graph.dart'; import 'package:pslab/view/widgets/wave_generator_main_controls.dart'; class WaveGeneratorScreen extends StatefulWidget { + final String sineWaveCircuit = 'assets/images/sin_wave_circuit.png'; + final String squareWaveCircuit = 'assets/images/square_wave_circuit.png'; const WaveGeneratorScreen({super.key}); @override @@ -19,6 +22,48 @@ class WaveGeneratorScreen extends StatefulWidget { class _WaveGeneratorScreenState extends State { AppLocalizations appLocalizations = getIt.get(); + bool _showGuide = false; + + void _hideInstrumentGuide() { + setState(() { + _showGuide = false; + }); + } + + List _getWaveGeneratorContent() { + return [ + InstrumentIntroText(text: appLocalizations.waveGeneratorIntro), + InstrumentIntroText( + text: appLocalizations.sineWaveCaption, + style: TextStyle(fontWeight: FontWeight.bold), + ), + InstrumentImage(imagePath: widget.sineWaveCircuit), + InstrumentBulletPoint(text: appLocalizations.sineWaveBulletPoint1), + InstrumentBulletPoint(text: appLocalizations.sineWaveBulletPoint2), + InstrumentBulletPoint(text: appLocalizations.sineWaveBulletPoint3), + InstrumentBulletPoint(text: appLocalizations.sineWaveBulletPoint4), + InstrumentBulletPoint(text: appLocalizations.sineWaveBulletPoint5), + InstrumentIntroText( + text: appLocalizations.squareWaveCaption, + style: TextStyle(fontWeight: FontWeight.bold), + ), + InstrumentImage(imagePath: widget.squareWaveCircuit), + InstrumentBulletPoint(text: appLocalizations.squareWaveBulletPoint1), + InstrumentBulletPoint(text: appLocalizations.squareWaveBulletPoint2), + InstrumentBulletPoint(text: appLocalizations.squareWaveBulletPoint3), + InstrumentBulletPoint(text: appLocalizations.squareWaveBulletPoint4), + InstrumentBulletPoint(text: appLocalizations.squareWaveBulletPoint5), + InstrumentIntroText( + text: appLocalizations.pwmCaption, + style: TextStyle(fontWeight: FontWeight.bold), + ), + InstrumentBulletPoint(text: appLocalizations.pwmBulletPoint1), + InstrumentBulletPoint(text: appLocalizations.pwmBulletPoint2), + InstrumentBulletPoint(text: appLocalizations.pwmBulletPoint3), + InstrumentBulletPoint(text: appLocalizations.pwmBulletPoint4), + ]; + } + @override Widget build(BuildContext context) { return SafeArea( @@ -30,127 +75,168 @@ class _WaveGeneratorScreenState extends State { ], child: Consumer( builder: (context, provider, _) { - return CommonScaffold( - title: appLocalizations.waveGenerator, - body: Container( - margin: const EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0), - child: Column( - children: [ - Expanded( - flex: 30, - child: Container( - color: chartBackgroundColor, - child: WaveGeneratorGraph(), - ), - ), - Column( + return Stack( + children: [ + CommonScaffold( + title: appLocalizations.waveGenerator, + body: Container( + margin: + const EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0), + child: Column( children: [ - provider.waveGeneratorConstants.modeSelected == - WaveConst.square - ? AnalogWaveformControls() - : DigitalWaveformControls(), - SizedBox( - height: 60, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: primaryRed, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - child: Text( - appLocalizations.produceSound, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - onPressed: () => {}, - ), - ), - const SizedBox(width: 4), - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: provider - .waveGeneratorConstants - .modeSelected == - WaveConst.square - ? buttonEnabledColor - : buttonDisabledColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - child: Text( - appLocalizations.analog, - style: TextStyle( - color: Colors.white, - fontSize: 14, + Expanded( + flex: 30, + child: Container( + color: chartBackgroundColor, + child: WaveGeneratorGraph(), + ), + ), + Column( + children: [ + provider.waveGeneratorConstants.modeSelected == + WaveConst.square + ? AnalogWaveformControls() + : DigitalWaveformControls(), + SizedBox( + height: 60, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: primaryRed, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(6), + ), + ), + child: Text( + appLocalizations.produceSound, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + onPressed: () => {}, ), ), - onPressed: () => { - setState( - () { - provider.waveGeneratorConstants - .modeSelected = WaveConst.square; - provider.propSelected = null; - provider.previewWave(); + const SizedBox(width: 4), + Expanded( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: provider + .waveGeneratorConstants + .modeSelected == + WaveConst.square + ? buttonEnabledColor + : buttonDisabledColor, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(6), + ), + ), + child: Text( + appLocalizations.analog, + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + onPressed: () => { + setState( + () { + provider.waveGeneratorConstants + .modeSelected = + WaveConst.square; + provider.propSelected = null; + provider.previewWave(); + }, + ), }, ), - }, - ), - ), - const SizedBox(width: 4), - Expanded( - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: provider - .waveGeneratorConstants - .modeSelected == - WaveConst.pwm - ? buttonEnabledColor - : buttonDisabledColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - child: Text( - appLocalizations.digital, - style: TextStyle( - color: Colors.white, - fontSize: 14, - ), ), - onPressed: () => { - setState( - () { - provider.waveGeneratorConstants - .modeSelected = WaveConst.pwm; - provider.propSelected = null; - provider.previewWave(); + const SizedBox(width: 4), + Expanded( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: provider + .waveGeneratorConstants + .modeSelected == + WaveConst.pwm + ? buttonEnabledColor + : buttonDisabledColor, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(6), + ), + ), + child: Text( + appLocalizations.digital, + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + onPressed: () => { + setState( + () { + provider.waveGeneratorConstants + .modeSelected = WaveConst.pwm; + provider.propSelected = null; + provider.previewWave(); + }, + ), }, ), - }, - ), + ), + ], ), - ], - ), + ), + ], + ), + Expanded( + flex: 40, + child: WaveGeneratorMainControls(), ), ], ), - Expanded( - flex: 40, - child: WaveGeneratorMainControls(), + ), + actions: [ + IconButton( + icon: Icon(Icons.play_arrow, color: Colors.white), + onPressed: () {}, ), + IconButton( + icon: Icon(Icons.save, color: Colors.white), + onPressed: () {}, + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert, color: Colors.white), + onSelected: (value) { + if (value == appLocalizations.showGuide) { + setState(() { + _showGuide = !_showGuide; + }); + } + }, + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: appLocalizations.showGuide, + child: Text(appLocalizations.showGuide), + ), + ], + ) ], ), - ), + if (_showGuide) + InstrumentOverviewDrawer( + instrumentName: appLocalizations.waveGenerator, + content: _getWaveGeneratorContent(), + onHide: _hideInstrumentGuide, + ), + ], ); }, ),