This commit is contained in:
kleponijo 2026-06-29 13:17:32 +07:00
parent 74a1609c21
commit a6e74d4da0
3 changed files with 191 additions and 85 deletions

View File

@ -49,7 +49,11 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
WatchWindSpeedStarted event, WatchWindSpeedStarted event,
Emitter<WindSpeedState> emit, Emitter<WindSpeedState> emit,
) async { ) async {
emit(state.copyWith(isLoading: true)); // Cancel stream lama dulu sebelum apapun
await _subscription?.cancel();
_subscription = null;
emit(state.copyWith(isLoading: true, currentSpeed: 0));
final deviceId = await _getDeviceId(); final deviceId = await _getDeviceId();
final historyMap = await _repository.getSensorHistoryWithKeys( final historyMap = await _repository.getSensorHistoryWithKeys(
@ -74,7 +78,7 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
history.isNotEmpty ? _getAlertLevel(history.last.speed) : 'Normal', history.isNotEmpty ? _getAlertLevel(history.last.speed) : 'Normal',
)); ));
await _subscription?.cancel(); // Subscribe stream baru
_subscription = _repository _subscription = _repository
.getSensorStream( .getSensorStream(
'anemometer/$deviceId/realtime', 'anemometer/$deviceId/realtime',
@ -90,6 +94,15 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
_WindSpeedRealtimeUpdated event, _WindSpeedRealtimeUpdated event,
Emitter<WindSpeedState> emit, Emitter<WindSpeedState> emit,
) { ) {
// Guard: abaikan data jika windweg 0 dan pulse 0 (ESP tidak kirim data nyata)
if (event.data.windwegKm == 0 && event.data.totalPulse == 0) {
emit(state.copyWith(
currentSpeed: 0,
alertLevel: 'Normal',
));
return;
}
final updated = List<double>.from(state.dailySpeeds); final updated = List<double>.from(state.dailySpeeds);
final index = DateTime.now().hour; final index = DateTime.now().hour;
double newValue = event.data.speed; double newValue = event.data.speed;

View File

@ -747,7 +747,7 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
const Icon(Icons.air, color: Colors.white, size: 46), const Icon(Icons.air, color: Colors.white, size: 46),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
speed.toStringAsFixed(1), speed.toStringAsFixed(2),
style: const TextStyle( style: const TextStyle(
fontSize: 80, fontSize: 80,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -755,11 +755,11 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
height: 1, height: 1,
), ),
), ),
const Text('m/s', const Text('km/h',
style: TextStyle(fontSize: 20, color: Colors.white70)), style: TextStyle(fontSize: 20, color: Colors.white70)),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'${(speed * 3.6).toStringAsFixed(1)} km/h', '${(speed / 3.6).toStringAsFixed(3)} m/s',
style: const TextStyle(fontSize: 14, color: Colors.white54), style: const TextStyle(fontSize: 14, color: Colors.white54),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
@ -1056,8 +1056,8 @@ class _HistoryTile extends StatelessWidget {
const _HistoryTile({required this.item, required this.index}); const _HistoryTile({required this.item, required this.index});
String get _alertLevel { String get _alertLevel {
if (item.speed >= 12.5) return 'Bahaya'; if (item.speed >= 45.0) return 'Bahaya';
if (item.speed >= 8.0) return 'Waspada'; if (item.speed >= 25.5) return 'Waspada';
return 'Normal'; return 'Normal';
} }
@ -1085,98 +1085,188 @@ class _HistoryTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final kmh = item.speed * 3.6;
final bg = index.isEven ? Colors.grey.shade50 : Colors.white; final bg = index.isEven ? Colors.grey.shade50 : Colors.white;
final hasTotal = item.totalWindwegKm > 0 || item.totalPulse > 0;
return Container( return Container(
color: bg, color: bg,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row( // child pakai Column, bukan Row langsung
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Nomor // Baris utama
SizedBox( Row(
width: 28, children: [
child: Text( // Nomor
'${index + 1}', SizedBox(
style: TextStyle( width: 28,
fontSize: 12, child: Text(
color: Colors.grey.shade400, '${index + 1}',
fontWeight: FontWeight.w600),
),
),
// Tanggal + jam
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('dd MMM yyyy', 'id_ID').format(item.timestamp),
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.w600),
),
Text(
DateFormat('HH:mm:ss').format(item.timestamp),
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 12,
color: Colors.grey.shade500, color: Colors.grey.shade400,
fontFamily: 'monospace'), fontWeight: FontWeight.w600,
),
), ),
], ),
),
// Tanggal + jam
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('dd MMM yyyy', 'id_ID').format(item.timestamp),
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.w600),
),
Text(
DateFormat('HH:mm:ss').format(item.timestamp),
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade500,
fontFamily: 'monospace',
),
),
],
),
),
// Avg + max windweg
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'avg ${item.windwegKm.toStringAsFixed(3)} km',
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.bold),
),
if (item.maxWindwegKm > 0)
Text(
'max ${item.maxWindwegKm.toStringAsFixed(3)} km',
style: TextStyle(
fontSize: 10, color: Colors.orange.shade600),
),
],
),
),
const SizedBox(width: 8),
// Badge status
Container(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
decoration: BoxDecoration(
color: _alertColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: _alertColor.withValues(alpha: 0.4)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_alertIcon, size: 11, color: _alertColor),
const SizedBox(width: 3),
Text(
_alertLevel,
style: TextStyle(
fontSize: 10,
color: _alertColor,
fontWeight: FontWeight.w700,
),
),
],
),
),
],
), ),
// Kecepatan // Baris kedua: total + pulsa + sampel
Expanded( if (hasTotal) ...[
flex: 2, const SizedBox(height: 6),
child: Column( Padding(
crossAxisAlignment: CrossAxisAlignment.end, padding: const EdgeInsets.only(left: 28),
children: [ child: Wrap(
Text( spacing: 6,
'${item.speed.toStringAsFixed(3)} m/s', runSpacing: 4,
style: const TextStyle( children: [
fontSize: 13, fontWeight: FontWeight.bold), _StatChip(
), icon: Icons.route_rounded,
Text( label: '${item.totalWindwegKm.toStringAsFixed(3)} km total',
'${kmh.toStringAsFixed(2)} km/h', tooltip: 'Total jarak angin periode ini',
style: TextStyle(fontSize: 11, color: Colors.grey.shade400), color: Colors.blue.shade700,
), ),
], _StatChip(
icon: Icons.bolt_rounded,
label: '${item.totalPulse} pulsa',
tooltip: 'Total pulsa terdeteksi',
color: Colors.purple.shade600,
),
if (item.sampleCount > 0)
_StatChip(
icon: Icons.analytics_outlined,
label: '${item.sampleCount} sampel',
tooltip: 'Jumlah interval average dalam periode ini',
color: Colors.teal.shade600,
),
],
),
), ),
), ],
const SizedBox(width: 10),
// Badge status
Container(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
decoration: BoxDecoration(
color: _alertColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: _alertColor.withValues(alpha: 0.4)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_alertIcon, size: 11, color: _alertColor),
const SizedBox(width: 3),
Text(
_alertLevel,
style: TextStyle(
fontSize: 10,
color: _alertColor,
fontWeight: FontWeight.w700),
),
],
),
),
], ],
), ),
); );
} }
} }
// Chip kecil untuk stat di history tile
class _StatChip extends StatelessWidget {
final IconData icon;
final String label;
final String tooltip;
final Color color;
const _StatChip({
required this.icon,
required this.label,
required this.tooltip,
required this.color,
});
@override
Widget build(BuildContext context) {
return Tooltip(
message: tooltip,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withValues(alpha: 0.25)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 11, color: color),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 10,
color: color,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
}
// //
// Delete Bottom Sheet // Delete Bottom Sheet
// //

View File

@ -1,12 +1,16 @@
class MyWindSpeed { class MyWindSpeed {
final double windwegKm; final double windwegKm;
final double totalWindwegKm;
final double maxWindwegKm; final double maxWindwegKm;
final int totalPulse;
final int sampleCount; final int sampleCount;
final DateTime timestamp; final DateTime timestamp;
MyWindSpeed({ MyWindSpeed({
required this.windwegKm, required this.windwegKm,
this.totalWindwegKm = 0.0,
this.maxWindwegKm = 0.0, this.maxWindwegKm = 0.0,
this.totalPulse = 0,
this.sampleCount = 0, this.sampleCount = 0,
required this.timestamp, required this.timestamp,
}); });
@ -22,9 +26,6 @@ class MyWindSpeed {
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) { factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
return MyWindSpeed( return MyWindSpeed(
// realtime windweg_km
// history avg_windweg_km
// average windweg_km
windwegKm: windwegKm:
((json['windweg_km'] ?? ((json['windweg_km'] ??
json['avg_windweg_km'] ?? json['avg_windweg_km'] ??
@ -32,7 +33,9 @@ class MyWindSpeed {
json['speed'] ?? json['speed'] ??
0)) 0))
.toDouble(), .toDouble(),
totalWindwegKm: (json['total_windweg_km'] ?? 0).toDouble(),
maxWindwegKm: (json['max_windweg_km'] ?? 0).toDouble(), maxWindwegKm: (json['max_windweg_km'] ?? 0).toDouble(),
totalPulse: (json['total_pulse'] ?? 0) as int,
sampleCount: (json['sample_count'] ?? 0) as int, sampleCount: (json['sample_count'] ?? 0) as int,
timestamp: DateTime.fromMillisecondsSinceEpoch( timestamp: DateTime.fromMillisecondsSinceEpoch(
(json['timestamp'] ?? 0) * 1000, (json['timestamp'] ?? 0) * 1000,