Skip to content

Commit 1169df0

Browse files
feat: implemented LUX meter (#2733)
1 parent b126c1f commit 1169df0

9 files changed

Lines changed: 653 additions & 3 deletions

lib/constants.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,9 @@ String maxValue = 'Max: ';
144144
String gyroscopeTitle = "Gyroscope";
145145
String gyroscopeAxisLabel = 'rad/s';
146146
String noData = 'No data available';
147+
String luxMeterTitle = 'Lux Meter';
148+
String builtIn = 'Built-In';
149+
String lx = 'Lx';
150+
String maxScaleError = 'Max Scale';
151+
String lightSensorError = 'Light sensor error:';
152+
String lightSensorInitialError = 'Failed to initialize light sensor:';

lib/main.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:pslab/view/connect_device_screen.dart';
77
import 'package:pslab/view/faq_screen.dart';
88
import 'package:pslab/view/gyroscope_screen.dart';
99
import 'package:pslab/view/instruments_screen.dart';
10+
import 'package:pslab/view/luxmeter_screen.dart';
1011
import 'package:pslab/view/oscilloscope_screen.dart';
1112
import 'package:pslab/view/settings_screen.dart';
1213
import 'package:pslab/view/about_us_screen.dart';
@@ -32,7 +33,6 @@ void main() {
3233

3334
class MyApp extends StatelessWidget {
3435
const MyApp({super.key});
35-
3636
@override
3737
Widget build(BuildContext context) {
3838
_preCacheImages(context);
@@ -52,6 +52,7 @@ class MyApp extends StatelessWidget {
5252
'/softwareLicenses': (context) => const SoftwareLicensesScreen(),
5353
'/accelerometer': (context) => const AccelerometerScreen(),
5454
'/gyroscope': (context) => const GyroscopeScreen(),
55+
'/luxmeter': (context) => const LuxMeterScreen(),
5556
},
5657
);
5758
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import 'dart:async';
2+
import 'dart:math';
3+
import 'package:fl_chart/fl_chart.dart';
4+
import 'package:flutter/material.dart';
5+
import 'package:pslab/others/logger_service.dart';
6+
import 'package:light/light.dart';
7+
import 'package:flutter/foundation.dart';
8+
import 'package:pslab/constants.dart';
9+
10+
class LuxMeterStateProvider extends ChangeNotifier {
11+
double _currentLux = 0.0;
12+
StreamSubscription? _lightSubscription;
13+
Timer? _timeTimer;
14+
final List<double> _luxData = [];
15+
final List<double> _timeData = [];
16+
final List<FlSpot> luxChartData = [];
17+
Light? _light;
18+
double _startTime = 0;
19+
double _currentTime = 0;
20+
final int _maxLength = 50;
21+
double _luxMin = 0;
22+
double _luxMax = 0;
23+
double _luxSum = 0;
24+
int _dataCount = 0;
25+
void initializeSensors() {
26+
try {
27+
_light = Light();
28+
_startTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
29+
_timeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
30+
_currentTime =
31+
(DateTime.now().millisecondsSinceEpoch / 1000.0) - _startTime;
32+
_updateData();
33+
notifyListeners();
34+
});
35+
_lightSubscription = _light!.lightSensorStream.listen(
36+
(int luxValue) {
37+
_currentLux = luxValue.toDouble();
38+
notifyListeners();
39+
},
40+
onError: (error) {
41+
logger.e(
42+
"$lightSensorError $error",
43+
);
44+
},
45+
cancelOnError: true,
46+
);
47+
} catch (e) {
48+
logger.e("$lightSensorInitialError $e");
49+
}
50+
}
51+
52+
void disposeSensors() {
53+
_lightSubscription?.cancel();
54+
_timeTimer?.cancel();
55+
}
56+
57+
@override
58+
void dispose() {
59+
disposeSensors();
60+
super.dispose();
61+
}
62+
63+
void _updateData() {
64+
final lux = _currentLux;
65+
final time = _currentTime;
66+
_luxData.add(lux);
67+
_timeData.add(time);
68+
_luxSum += lux;
69+
_dataCount++;
70+
if (_luxData.length > _maxLength) {
71+
final removedValue = _luxData.removeAt(0);
72+
_timeData.removeAt(0);
73+
_luxSum -= removedValue;
74+
_dataCount--;
75+
}
76+
if (_luxData.isNotEmpty) {
77+
_luxMin = _luxData.reduce(min);
78+
_luxMax = _luxData.reduce(max);
79+
}
80+
luxChartData.clear();
81+
for (int i = 0; i < _luxData.length; i++) {
82+
luxChartData.add(FlSpot(_timeData[i], _luxData[i]));
83+
}
84+
notifyListeners();
85+
}
86+
87+
double getCurrentLux() => _currentLux;
88+
double getMinLux() => _luxMin;
89+
double getMaxLux() => _luxMax;
90+
double getAverageLux() => _dataCount > 0 ? _luxSum / _dataCount : 0.0;
91+
List<FlSpot> getLuxChartData() => luxChartData;
92+
int getDataLength() => luxChartData.length;
93+
double getCurrentTime() => _currentTime;
94+
double getMaxTime() => _timeData.isNotEmpty ? _timeData.last : 0;
95+
double getMinTime() => _timeData.isNotEmpty ? _timeData.first : 0;
96+
double getTimeInterval() {
97+
if (_currentTime <= 10) return 2;
98+
if (_currentTime <= 30) return 5;
99+
return 10;
100+
}
101+
}

lib/view/instruments_screen.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'package:pslab/view/widgets/main_scaffold_widget.dart';
77

88
class InstrumentsScreen extends StatefulWidget {
99
const InstrumentsScreen({super.key});
10-
1110
@override
1211
State<StatefulWidget> createState() => _InstrumentsScreenState();
1312
}
@@ -38,6 +37,18 @@ class _InstrumentsScreenState extends State<InstrumentsScreen> {
3837
);
3938
}
4039
break;
40+
case 6:
41+
if (Navigator.canPop(context) &&
42+
ModalRoute.of(context)?.settings.name == '/luxmeter') {
43+
Navigator.popUntil(context, ModalRoute.withName('/luxmeter'));
44+
} else {
45+
Navigator.pushNamedAndRemoveUntil(
46+
context,
47+
'/luxmeter',
48+
(route) => route.isFirst,
49+
);
50+
}
51+
break;
4152
case 10:
4253
if (Navigator.canPop(context) &&
4354
ModalRoute.of(context)?.settings.name == '/gyroscope') {

lib/view/luxmeter_screen.dart

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:provider/provider.dart';
3+
import 'package:pslab/constants.dart';
4+
import 'package:pslab/providers/luxmeter_state_provider.dart';
5+
import 'package:pslab/view/widgets/common_scaffold_widget.dart';
6+
import 'package:pslab/view/widgets/luxmeter_card.dart';
7+
import 'package:fl_chart/fl_chart.dart';
8+
9+
class LuxMeterScreen extends StatefulWidget {
10+
const LuxMeterScreen({super.key});
11+
@override
12+
State<StatefulWidget> createState() => _LuxMeterScreenState();
13+
}
14+
15+
class _LuxMeterScreenState extends State<LuxMeterScreen> {
16+
@override
17+
Widget build(BuildContext context) {
18+
return MultiProvider(
19+
providers: [
20+
ChangeNotifierProvider<LuxMeterStateProvider>(
21+
create: (_) => LuxMeterStateProvider()..initializeSensors(),
22+
),
23+
],
24+
child: CommonScaffold(
25+
title: luxMeterTitle,
26+
body: SafeArea(
27+
child: Column(
28+
children: [
29+
const Expanded(
30+
flex: 45,
31+
child: LuxMeterCard(),
32+
),
33+
Expanded(
34+
flex: 55,
35+
child: _buildChartSection(),
36+
),
37+
],
38+
),
39+
),
40+
),
41+
);
42+
}
43+
44+
Widget _buildChartSection() {
45+
return Consumer<LuxMeterStateProvider>(
46+
builder: (context, provider, child) {
47+
final screenWidth = MediaQuery.of(context).size.width;
48+
final cardMargin = screenWidth < 400 ? 8.0 : 16.0;
49+
List<FlSpot> spots = provider.getLuxChartData();
50+
double maxLux = provider.getMaxLux();
51+
double maxTime = provider.getMaxTime();
52+
double minTime = provider.getMinTime();
53+
double timeInterval = provider.getTimeInterval();
54+
55+
return Container(
56+
margin: EdgeInsets.fromLTRB(cardMargin, 0, cardMargin, cardMargin),
57+
padding: EdgeInsets.all(cardMargin),
58+
decoration: BoxDecoration(
59+
color: Colors.black,
60+
borderRadius: BorderRadius.circular(8),
61+
),
62+
child: _buildChart(
63+
screenWidth, maxLux, maxTime, minTime, timeInterval, spots),
64+
);
65+
},
66+
);
67+
}
68+
69+
Widget sideTitleWidgets(double value, TitleMeta meta) {
70+
final screenWidth = MediaQuery.of(context).size.width;
71+
final fontSize = screenWidth < 400
72+
? 7.0
73+
: screenWidth < 600
74+
? 8.0
75+
: 9.0;
76+
final style = TextStyle(
77+
color: Colors.white,
78+
fontSize: fontSize,
79+
);
80+
String timeText;
81+
if (value < 60) {
82+
timeText = '${value.toInt()}s';
83+
} else if (value < 3600) {
84+
int minutes = (value / 60).floor();
85+
int seconds = (value % 60).toInt();
86+
timeText = '${minutes}m${seconds}s';
87+
} else {
88+
int hours = (value / 3600).floor();
89+
int minutes = ((value % 3600) / 60).floor();
90+
timeText = '${hours}h${minutes}m';
91+
}
92+
return SideTitleWidget(
93+
meta: meta,
94+
child: Text(
95+
maxLines: 1,
96+
timeText,
97+
style: style,
98+
),
99+
);
100+
}
101+
102+
Widget _buildChart(double screenWidth, double maxLux, double maxTime,
103+
double minTime, double timeInterval, List<FlSpot> spots) {
104+
final chartFontSize = screenWidth < 400
105+
? 8.0
106+
: screenWidth < 600
107+
? 9.0
108+
: 10.0;
109+
final axisNameFontSize = screenWidth < 400 ? 9.0 : 10.0;
110+
final reservedSizeBottom = screenWidth < 400 ? 25.0 : 30.0;
111+
final reservedSizeLeft = screenWidth < 400 ? 20.0 : 25.0;
112+
return LineChart(
113+
LineChartData(
114+
backgroundColor: Colors.black,
115+
titlesData: FlTitlesData(
116+
show: true,
117+
topTitles: AxisTitles(
118+
axisNameWidget: Padding(
119+
padding: EdgeInsets.only(left: screenWidth < 400 ? 15 : 25),
120+
child: Text(
121+
timeAxisLabel,
122+
style: TextStyle(
123+
fontSize: axisNameFontSize,
124+
color: Colors.white,
125+
fontWeight: FontWeight.bold,
126+
),
127+
),
128+
),
129+
axisNameSize: screenWidth < 400 ? 18 : 20,
130+
),
131+
bottomTitles: AxisTitles(
132+
sideTitles: SideTitles(
133+
showTitles: true,
134+
reservedSize: reservedSizeBottom,
135+
getTitlesWidget: sideTitleWidgets,
136+
interval: timeInterval,
137+
),
138+
),
139+
leftTitles: AxisTitles(
140+
axisNameWidget: Text(
141+
lx,
142+
style: TextStyle(
143+
fontSize: axisNameFontSize,
144+
color: Colors.white,
145+
fontWeight: FontWeight.bold,
146+
),
147+
),
148+
sideTitles: SideTitles(
149+
reservedSize: reservedSizeLeft,
150+
showTitles: true,
151+
getTitlesWidget: (value, meta) {
152+
return SideTitleWidget(
153+
meta: meta,
154+
child: Text(
155+
value.toInt().toString(),
156+
style: TextStyle(
157+
color: Colors.white,
158+
fontSize: chartFontSize,
159+
),
160+
),
161+
);
162+
},
163+
interval: maxLux > 0 ? (maxLux / 5).ceilToDouble() : 10,
164+
),
165+
),
166+
rightTitles: const AxisTitles(
167+
sideTitles: SideTitles(showTitles: false),
168+
),
169+
),
170+
gridData: FlGridData(
171+
show: true,
172+
drawHorizontalLine: true,
173+
drawVerticalLine: true,
174+
horizontalInterval: maxLux > 0 ? (maxLux / 5).ceilToDouble() : 10,
175+
verticalInterval: timeInterval,
176+
),
177+
borderData: FlBorderData(
178+
show: true,
179+
border: const Border(
180+
bottom: BorderSide(color: Colors.white38),
181+
left: BorderSide(color: Colors.white38),
182+
top: BorderSide(color: Colors.white38),
183+
right: BorderSide(color: Colors.white38),
184+
),
185+
),
186+
minY: 0,
187+
maxY: maxLux > 0 ? (maxLux * 1.1) : 100,
188+
maxX: maxTime > 0 ? maxTime : 10,
189+
minX: minTime,
190+
clipData: const FlClipData.all(),
191+
lineBarsData: [
192+
LineChartBarData(
193+
spots: spots,
194+
isCurved: true,
195+
color: Colors.cyan,
196+
barWidth: screenWidth < 400 ? 1.5 : 2.0,
197+
isStrokeCapRound: true,
198+
dotData: const FlDotData(show: false),
199+
belowBarData: BarAreaData(show: false),
200+
),
201+
],
202+
),
203+
);
204+
}
205+
}

0 commit comments

Comments
 (0)