diff --git a/assets/images/vl53l0x.jpg b/assets/images/vl53l0x.jpg new file mode 100644 index 000000000..236573739 Binary files /dev/null and b/assets/images/vl53l0x.jpg differ diff --git a/lib/communication/sensors/vl53l0x.dart b/lib/communication/sensors/vl53l0x.dart new file mode 100644 index 000000000..25fd49fb9 --- /dev/null +++ b/lib/communication/sensors/vl53l0x.dart @@ -0,0 +1,311 @@ +import 'package:pslab/communication/peripherals/i2c.dart'; +import 'package:pslab/communication/science_lab.dart'; +import 'package:pslab/others/logger_service.dart'; + +class VL53L0X { + static const String tag = "VL53L0X"; + static const int address = 0x29; + static const int sysrangeStart = 0x00; + static const int systemSequenceConfig = 0x01; + static const int systemInterruptConfigGpio = 0x0A; + static const int gpioHvMuxActiveHigh = 0x84; + static const int systemInterruptClear = 0x0B; + static const int resultInterruptStatus = 0x13; + static const int resultRangeStatus = 0x14; + static const int msrcConfigControl = 0x60; + static const int globalConfigSpadEnablesRef0 = 0xB0; + static const int globalConfigRefEnStartSelect = 0xB6; + static const int dynamicSpadNumRequestedRefSpad = 0x4E; + static const int dynamicSpadRefEnStartOffset = 0x4F; + static const int disableSignalRateMsrc = 0x2; + static const int disableSignalRatePreRange = 0x10; + static const int maybeTimerReg = 0x83; + static const int ioTimeout = 10; + static const List> spadConfig = [ + [0xFF, 0x01], + [dynamicSpadRefEnStartOffset, 0x00], + [dynamicSpadNumRequestedRefSpad, 0x2C], + [0xFF, 0x00], + [globalConfigRefEnStartSelect, 0xB4] + ]; + static const List> tuningConfig = [ + [0xFF, 0x01], + [0x00, 0x00], + [0xFF, 0x00], + [0x09, 0x00], + [0x10, 0x00], + [0x11, 0x00], + [0x24, 0x01], + [0x25, 0xFF], + [0x75, 0x00], + [0xFF, 0x01], + [0x4E, 0x2C], + [0x48, 0x00], + [0x30, 0x20], + [0xFF, 0x00], + [0x30, 0x09], + [0x54, 0x00], + [0x31, 0x04], + [0x32, 0x03], + [0x40, 0x83], + [0x46, 0x25], + [0x60, 0x00], + [0x27, 0x00], + [0x50, 0x06], + [0x51, 0x00], + [0x52, 0x96], + [0x56, 0x08], + [0x57, 0x30], + [0x61, 0x00], + [0x62, 0x00], + [0x64, 0x00], + [0x65, 0x00], + [0x66, 0xA0], + [0xFF, 0x01], + [0x22, 0x32], + [0x47, 0x14], + [0x49, 0xFF], + [0x4A, 0x00], + [0xFF, 0x00], + [0x7A, 0x0A], + [0x7B, 0x00], + [0x78, 0x21], + [0xFF, 0x01], + [0x23, 0x34], + [0x42, 0x00], + [0x44, 0xFF], + [0x45, 0x26], + [0x46, 0x05], + [0x40, 0x40], + [0x0E, 0x06], + [0x20, 0x1A], + [0x43, 0x40], + [0xFF, 0x00], + [0x34, 0x03], + [0x35, 0x44], + [0xFF, 0x01], + [0x31, 0x04], + [0x4B, 0x09], + [0x4C, 0x05], + [0x4D, 0x04], + [0xFF, 0x00], + [0x44, 0x00], + [0x45, 0x20], + [0x47, 0x08], + [0x48, 0x28], + [0x67, 0x00], + [0x70, 0x04], + [0x71, 0x01], + [0x72, 0xFE], + [0x76, 0x00], + [0x77, 0x00], + [0xFF, 0x01], + [0x0D, 0x01], + [0xFF, 0x00], + [0x80, 0x01], + [0x01, 0xF8], + [0xFF, 0x01], + [0x8E, 0x01], + [0x00, 0x01], + [0xFF, 0x00], + [0x80, 0x00] + ]; + static const List> spad1 = [ + [0x80, 0x01], + [0xFF, 0x01], + [0x00, 0x00], + [0xFF, 0x06] + ]; + static const List> spad2 = [ + [0xFF, 0x07], + [0x81, 0x01], + [0x80, 0x01], + [0x94, 0x6B], + [maybeTimerReg, 0x00] + ]; + static const List> spad3 = [ + [0x81, 0x00], + [0xFF, 0x06] + ]; + static const List> spad4 = [ + [0xFF, 0x01], + [0x00, 0x01], + [0xFF, 0x00], + [0x80, 0x00] + ]; + final I2C i2c; + late int stopByte; + VL53L0X._(this.i2c); + static Future create(I2C i2c, ScienceLab scienceLab) async { + final vl53l0x = VL53L0X._(i2c); + await vl53l0x._initialize(scienceLab); + return vl53l0x; + } + + Future _initialize(ScienceLab scienceLab) async { + if (!scienceLab.isConnected()) { + throw Exception("ScienceLab not connected"); + } + try { + List> initSequence = [ + [0x88, 0x00], + [0x80, 0x01], + [0xFF, 0x01], + [0x00, 0x00] + ]; + for (List regValPair in initSequence) { + await i2c.write(address, [regValPair[1]], regValPair[0]); + } + stopByte = await i2c.readByte(address, 0x91); + List> postReadSequence = [ + [0x00, 0x01], + [0xFF, 0x00], + [0x80, 0x00] + ]; + for (List regValPair in postReadSequence) { + await i2c.write(address, [regValPair[1]], regValPair[0]); + } + int configControl = await i2c.readByte(address, msrcConfigControl) | + (disableSignalRateMsrc | disableSignalRatePreRange); + await i2c.write(address, [configControl], msrcConfigControl); + await i2c.write(address, [0xFF], systemSequenceConfig); + await _spadConfig(); + for (List regValPair in tuningConfig) { + await i2c.write(address, [regValPair[1]], regValPair[0]); + } + await i2c.write(address, [0x04], systemInterruptConfigGpio); + int gpioHvMux = await i2c.readByte(address, gpioHvMuxActiveHigh); + await i2c.write(address, [gpioHvMux & ~0x10], gpioHvMuxActiveHigh); + await i2c.write(address, [0x01], systemInterruptClear); + await i2c.write(address, [0xE8], systemSequenceConfig); + await i2c.write(address, [0x01], systemSequenceConfig); + await _performSingleRefCalibration(0x40); + await i2c.write(address, [0x01], systemSequenceConfig); + await i2c.write(address, [0x02], systemSequenceConfig); + await _performSingleRefCalibration(0x00); + await i2c.write(address, [0xE8], systemSequenceConfig); + logger.d("VL53L0X initialized successfully"); + } catch (e) { + logger.e("Error initializing VL53L0X: $e"); + rethrow; + } + } + + Future> _getSpadInfo() async { + for (List regValPair in spad1) { + await i2c.write(address, [regValPair[1]], regValPair[0]); + } + int uu = await i2c.readByte(address, maybeTimerReg) | 0x04; + await i2c.write(address, [uu], maybeTimerReg); + for (List regValPair in spad2) { + await i2c.write(address, [regValPair[1]], regValPair[0]); + } + int start = DateTime.now().millisecondsSinceEpoch; + while (await i2c.readByte(address, maybeTimerReg) == 0x00) { + if (ioTimeout > 0 && + (DateTime.now().millisecondsSinceEpoch - start) / 1000.0 >= + ioTimeout) { + logger.e("Timeout waiting for VL53L0X!"); + break; + } + } + await i2c.write(address, [0x01], maybeTimerReg); + int tmp = await i2c.readByte(address, 0x92); + int count = tmp & 0x7F; + bool isAperture = ((tmp >> 7) & 0x01) == 1; + for (List regValPair in spad3) { + await i2c.write(address, [regValPair[1]], regValPair[0]); + } + int vv = await i2c.readByte(address, maybeTimerReg) & ~0x04; + await i2c.write(address, [vv], maybeTimerReg); + for (List regValPair in spad4) { + await i2c.write(address, [regValPair[1]], regValPair[0]); + } + return [count, isAperture ? 1 : 0]; + } + + Future _spadConfig() async { + List spadInfo = await _getSpadInfo(); + int spadCount = spadInfo[0]; + int spadIsAperture = spadInfo[1]; + await i2c.write(address, [0], globalConfigSpadEnablesRef0); + List spadMap = + await i2c.readBulk(address, globalConfigSpadEnablesRef0, 6); + for (List regValPair in spadConfig) { + await i2c.write(address, [regValPair[1]], regValPair[0]); + } + int firstSpadToEnable = (spadIsAperture == 1) ? 12 : 0; + int spadsEnabled = 0; + for (int i = 0; i < 48; i++) { + int index = i ~/ 8; + if (i < firstSpadToEnable || spadsEnabled == spadCount) { + spadMap[index] = spadMap[index] & ~(1 << (i % 8)); + } else if (((spadMap[index] >> (i % 8)) & 0x1) > 0) { + spadsEnabled++; + } + } + await i2c.writeBulk(address, spadMap); + } + + Future _performSingleRefCalibration(int vhvInitByte) async { + await i2c.write(address, [0x01 | vhvInitByte & 0xFF], sysrangeStart); + int start = DateTime.now().millisecondsSinceEpoch; + while ((await i2c.readByte(address, resultInterruptStatus) & 0x07) == 0) { + if (ioTimeout > 0 && + (DateTime.now().millisecondsSinceEpoch - start) / 1000.0 >= + ioTimeout) { + logger.e("Timeout waiting for VL53L0X!"); + break; + } + } + await i2c.write(address, [0x01], systemInterruptClear); + await i2c.write(address, [0x00], sysrangeStart); + } + + Future getRaw() async { + try { + List> startSequence = [ + [0x80, 0x01], + [0xFF, 0x01], + [0x00, 0x00], + [0x91, stopByte], + [0x00, 0x01], + [0xFF, 0x00], + [0x80, 0x00], + [sysrangeStart, 0x01] + ]; + for (List regValPair in startSequence) { + await i2c.write(address, [regValPair[1]], regValPair[0]); + } + int start = DateTime.now().millisecondsSinceEpoch; + while ((await i2c.readByte(address, sysrangeStart) & 0x01) > 0) { + if (ioTimeout > 0 && + (DateTime.now().millisecondsSinceEpoch - start) / 1000.0 >= + ioTimeout) { + logger.e("Timeout waiting for VL53L0X!"); + break; + } + } + start = DateTime.now().millisecondsSinceEpoch; + while ((await i2c.readByte(address, resultInterruptStatus) & 0x07) == 0) { + if (ioTimeout > 0 && + (DateTime.now().millisecondsSinceEpoch - start) / 1000.0 >= + ioTimeout) { + logger.e("Timeout waiting for VL53L0X!"); + break; + } + } + List data = await i2c.readBulk(address, resultRangeStatus + 10, 2); + await i2c.write(address, [0x01], systemInterruptClear); + return ((data[0] & 0xFF) << 8) | (data[1] & 0xFF); + } catch (e) { + logger.e("Error reading VL53L0X raw data: $e"); + rethrow; + } + } + + Future getDistance() async { + int rawValue = await getRaw(); + return rawValue.toDouble(); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 19f6ed6e7..6ae9a1213 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -390,5 +390,7 @@ "altitudeUnitLabel" : "m", "time" : "Time", "notAvailable" : "N/A", - "estimated" : "Estimated" + "estimated" : "Estimated", + "distance" : "Distance", + "distanceUnitLabel" : "mm" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8cc552eb7..63f9d796b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2439,6 +2439,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Estimated'** String get estimated; + + /// No description provided for @distance. + /// + /// In en, this message translates to: + /// **'Distance'** + String get distance; + + /// No description provided for @distanceUnitLabel. + /// + /// In en, this message translates to: + /// **'mm'** + String get distanceUnitLabel; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 636ecb28b..c2d1613d0 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1253,4 +1253,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get estimated => 'Estimated'; + + @override + String get distance => 'Distance'; + + @override + String get distanceUnitLabel => 'mm'; } diff --git a/lib/providers/vl53l0x_provider.dart b/lib/providers/vl53l0x_provider.dart new file mode 100644 index 000000000..8108b499d --- /dev/null +++ b/lib/providers/vl53l0x_provider.dart @@ -0,0 +1,170 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:pslab/communication/peripherals/i2c.dart'; +import 'package:pslab/communication/science_lab.dart'; +import 'package:pslab/communication/sensors/vl53l0x.dart'; +import 'package:pslab/others/logger_service.dart'; +import 'package:pslab/models/chart_data_points.dart'; + +import '../l10n/app_localizations.dart'; +import 'locator.dart'; + +class VL53L0XProvider extends ChangeNotifier { + AppLocalizations appLocalizations = getIt.get(); + static const int maxDataPoints = 1000; + VL53L0X? _sensor; + Timer? _timer; + + bool _isRunning = false; + bool _isLooping = false; + int _timegapMs = 500; + int _numberOfReadings = 100; + int _collectedReadings = 0; + + double _distance = 0.0; + double _currentTime = 0.0; + final List _distanceData = []; + + bool get isRunning => _isRunning; + bool get isLooping => _isLooping; + int get timegapMs => _timegapMs; + int get numberOfReadings => _numberOfReadings; + int get collectedReadings => _collectedReadings; + double get distance => _distance; + List get distanceData => List.unmodifiable(_distanceData); + + Future initializeSensors({ + required Function(String) onError, + I2C? i2c, + ScienceLab? scienceLab, + }) async { + try { + if (i2c == null || scienceLab == null) { + onError(appLocalizations.pslabNotConnected); + logger.w('I2C or ScienceLab not available'); + return; + } + if (!scienceLab.isConnected()) { + onError(appLocalizations.pslabNotConnected); + logger.w("Sciencelab not connected"); + return; + } + _sensor = await VL53L0X.create(i2c, scienceLab); + logger.d('VL53L0X sensor initialized successfully'); + } catch (e) { + logger.e('Failed to initialize VL53L0X: $e'); + } + } + + void toggleDataCollection() { + if (_isRunning) { + _stopDataCollection(); + } else { + _startDataCollection(); + } + } + + void _startDataCollection() { + if (_sensor == null) return; + + _isRunning = true; + _collectedReadings = 0; + + _timer = Timer.periodic(Duration(milliseconds: _timegapMs), (timer) async { + try { + await _collectData(); + _collectedReadings++; + + if (!_isLooping && _collectedReadings >= _numberOfReadings) { + _stopDataCollection(); + } + + if (_isLooping && _distanceData.length >= maxDataPoints) { + _removeOldestDataPoints(); + } + } catch (e) { + logger.e('Error collecting VL53L0X data: $e'); + } + }); + notifyListeners(); + } + + void _stopDataCollection() { + _isRunning = false; + _timer?.cancel(); + _timer = null; + notifyListeners(); + } + + Future _collectData() async { + if (_sensor == null) return; + + try { + double newDistance = await _sensor!.getDistance(); + _distance = newDistance; + + _currentTime += _timegapMs / 1000.0; + + _addDataPoint(_distanceData, _distance); + + notifyListeners(); + } catch (e) { + logger.e('Error in _collectData: $e'); + rethrow; + } + } + + void _addDataPoint(List dataList, double value) { + dataList.add(ChartDataPoint(_currentTime, value)); + if (dataList.length > 50) { + dataList.removeAt(0); + } + } + + void _removeOldestDataPoints() { + const keepPoints = 800; + if (_distanceData.length > keepPoints) { + final removeCount = _distanceData.length - keepPoints; + _distanceData.removeRange(0, removeCount); + } + } + + void toggleLooping() { + _isLooping = !_isLooping; + notifyListeners(); + } + + void setTimegap(int newTimegap) { + _timegapMs = newTimegap; + + if (_isRunning) { + _stopDataCollection(); + _startDataCollection(); + } + + notifyListeners(); + } + + void setNumberOfReadings(int newNumber) { + _numberOfReadings = newNumber; + notifyListeners(); + } + + void clearData() { + _distanceData.clear(); + _distance = 0.0; + _currentTime = 0.0; + _collectedReadings = 0; + notifyListeners(); + } + + bool get isCollectionComplete { + return !_isLooping && _collectedReadings >= _numberOfReadings; + } + + @override + void dispose() { + _stopDataCollection(); + super.dispose(); + } +} diff --git a/lib/view/sensors_screen.dart b/lib/view/sensors_screen.dart index 2c4876fe4..b5feec454 100644 --- a/lib/view/sensors_screen.dart +++ b/lib/view/sensors_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pslab/view/bmp180_screen.dart'; +import 'package:pslab/view/vl53l0x_screen.dart'; import 'package:pslab/view/widgets/common_scaffold_widget.dart'; import '../../providers/board_state_provider.dart'; import '../l10n/app_localizations.dart'; @@ -221,6 +222,9 @@ class _SensorsScreenState extends State { case 'BMP180': targetScreen = const BMP180Screen(); break; + case 'VL53L0X': + targetScreen = const VL53L0XScreen(); + break; default: ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/view/vl53l0x_screen.dart b/lib/view/vl53l0x_screen.dart new file mode 100644 index 000000000..72ecc975d --- /dev/null +++ b/lib/view/vl53l0x_screen.dart @@ -0,0 +1,278 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/providers/vl53l0x_provider.dart'; +import 'package:pslab/view/widgets/common_scaffold_widget.dart'; +import 'package:pslab/view/widgets/sensor_chart_widget.dart'; +import 'package:pslab/view/widgets/sensor_controls.dart'; +import 'package:pslab/communication/peripherals/i2c.dart'; +import 'package:pslab/communication/science_lab.dart'; +import 'package:pslab/providers/locator.dart'; +import 'package:pslab/others/logger_service.dart'; +import '../l10n/app_localizations.dart'; +import '../theme/colors.dart'; + +class VL53L0XScreen extends StatefulWidget { + const VL53L0XScreen({super.key}); + @override + State createState() => _VL53L0XScreenState(); +} + +class _VL53L0XScreenState extends State { + AppLocalizations appLocalizations = getIt.get(); + String sensorImage = 'assets/images/vl53l0x.jpg'; + I2C? _i2c; + ScienceLab? _scienceLab; + late VL53L0XProvider _provider; + @override + void initState() { + super.initState(); + _initializeScienceLab(); + } + + void _initializeScienceLab() async { + try { + _scienceLab = getIt.get(); + if (_scienceLab != null && _scienceLab!.isConnected()) { + _i2c = I2C(_scienceLab!.mPacketHandler); + } + } catch (e) { + logger.e('Error initializing ScienceLab: $e'); + } + } + + void _showSensorErrorSnackbar(String message) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + message, + style: TextStyle(color: snackBarContentColor), + ), + backgroundColor: snackBarBackgroundColor, + duration: const Duration(milliseconds: 500), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + void _showSuccessSnackbar(String message) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + message, + style: TextStyle(color: snackBarContentColor), + ), + backgroundColor: snackBarBackgroundColor, + duration: const Duration(milliseconds: 500), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) { + _provider = VL53L0XProvider() + ..initializeSensors( + onError: _showSensorErrorSnackbar, + i2c: _i2c, + scienceLab: _scienceLab, + ); + return _provider; + }, + child: Consumer( + builder: (context, provider, child) { + return CommonScaffold( + title: 'VL53L0X', + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildRawDataSection(provider), + const SizedBox(height: 24), + SensorChartWidget( + title: + '${appLocalizations.plot} - ${appLocalizations.distance}', + yAxisLabel: + '${appLocalizations.distance} (${appLocalizations.distanceUnitLabel})', + lineColor: primaryRed, + data: provider.distanceData, + unit: ' ${appLocalizations.distanceUnitLabel}', + maxDataPoints: provider.numberOfReadings, + showDots: true, + ), + const SizedBox(height: 100), + ], + ), + ), + ), + SensorControlsWidget( + isPlaying: provider.isRunning, + isLooping: provider.isLooping, + timegapMs: provider.timegapMs, + numberOfReadings: provider.numberOfReadings, + onPlayPause: () { + provider.toggleDataCollection(); + }, + onLoop: provider.toggleLooping, + onTimegapChanged: provider.setTimegap, + onNumberOfReadingsChanged: provider.setNumberOfReadings, + onClearData: () { + provider.clearData(); + _showSuccessSnackbar(appLocalizations.dataCleared); + }, + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildRawDataSection(VL53L0XProvider provider) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: cardBackgroundColor, + borderRadius: BorderRadius.zero, + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(50), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: primaryRed, + borderRadius: const BorderRadius.only( + topLeft: Radius.zero, + topRight: Radius.zero, + ), + ), + child: Row( + children: [ + Text( + appLocalizations.rawData, + style: TextStyle( + color: appBarContentColor, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + if (provider.isRunning) + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: appBarContentColor, + shape: BoxShape.circle, + ), + ), + ], + ), + ), + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Column( + children: [ + _buildDataCard( + appLocalizations.distance, + provider.distance.toStringAsFixed(1), + ), + ], + ), + ), + const SizedBox(width: 24), + SizedBox( + width: 80, + height: 80, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.asset( + sensorImage, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.sensors, + size: 40, + color: sensorControlsTextBox, + ); + }, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildDataCard(String label, String value) { + return Row( + children: [ + SizedBox( + width: 100, + child: Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: blackTextColor, + ), + ), + ), + Expanded( + flex: 3, + child: Container( + height: 36, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: sensorControlsTextBox), + borderRadius: BorderRadius.circular(4), + color: cardBackgroundColor, + ), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + value, + style: TextStyle( + fontSize: 14, + color: blackTextColor, + ), + ), + ), + ), + ), + ], + ); + } + + @override + void dispose() { + super.dispose(); + } +}