From a6e74d4da0b2e0d9c28f017be69bd2a1b1a376a1 Mon Sep 17 00:00:00 2001 From: kleponijo Date: Mon, 29 Jun 2026 13:17:32 +0700 Subject: [PATCH] oke --- .../wind_speed/blocs/wind_speed_bloc.dart | 17 +- .../wind_speed/views/wind_speed_screen.dart | 250 ++++++++++++------ .../lib/src/models/wind_speed.dart | 9 +- 3 files changed, 191 insertions(+), 85 deletions(-) diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart index 3a7e40d..0c0e1c0 100644 --- a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart +++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart @@ -49,7 +49,11 @@ class WindSpeedBloc extends Bloc { WatchWindSpeedStarted event, Emitter emit, ) 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 historyMap = await _repository.getSensorHistoryWithKeys( @@ -74,7 +78,7 @@ class WindSpeedBloc extends Bloc { history.isNotEmpty ? _getAlertLevel(history.last.speed) : 'Normal', )); - await _subscription?.cancel(); + // Subscribe stream baru _subscription = _repository .getSensorStream( 'anemometer/$deviceId/realtime', @@ -90,6 +94,15 @@ class WindSpeedBloc extends Bloc { _WindSpeedRealtimeUpdated event, Emitter 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.from(state.dailySpeeds); final index = DateTime.now().hour; double newValue = event.data.speed; diff --git a/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart b/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart index 55cab91..89ace8e 100644 --- a/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart +++ b/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart @@ -747,7 +747,7 @@ class _WindSpeedScreenState extends State { const Icon(Icons.air, color: Colors.white, size: 46), const SizedBox(height: 8), Text( - speed.toStringAsFixed(1), + speed.toStringAsFixed(2), style: const TextStyle( fontSize: 80, fontWeight: FontWeight.bold, @@ -755,11 +755,11 @@ class _WindSpeedScreenState extends State { height: 1, ), ), - const Text('m/s', + const Text('km/h', style: TextStyle(fontSize: 20, color: Colors.white70)), const SizedBox(height: 4), Text( - '${(speed * 3.6).toStringAsFixed(1)} km/h', + '${(speed / 3.6).toStringAsFixed(3)} m/s', style: const TextStyle(fontSize: 14, color: Colors.white54), ), const SizedBox(height: 14), @@ -1056,8 +1056,8 @@ class _HistoryTile extends StatelessWidget { const _HistoryTile({required this.item, required this.index}); String get _alertLevel { - if (item.speed >= 12.5) return 'Bahaya'; - if (item.speed >= 8.0) return 'Waspada'; + if (item.speed >= 45.0) return 'Bahaya'; + if (item.speed >= 25.5) return 'Waspada'; return 'Normal'; } @@ -1085,98 +1085,188 @@ class _HistoryTile extends StatelessWidget { @override Widget build(BuildContext context) { - final kmh = item.speed * 3.6; final bg = index.isEven ? Colors.grey.shade50 : Colors.white; + final hasTotal = item.totalWindwegKm > 0 || item.totalPulse > 0; return Container( color: bg, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( + // ← child pakai Column, bukan Row langsung + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Nomor - SizedBox( - width: 28, - child: Text( - '${index + 1}', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade400, - 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), + // ── Baris utama ─────────────────────────────────── + Row( + children: [ + // Nomor + SizedBox( + width: 28, + child: Text( + '${index + 1}', style: TextStyle( - fontSize: 11, - color: Colors.grey.shade500, - fontFamily: 'monospace'), + fontSize: 12, + color: Colors.grey.shade400, + 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 - Expanded( - flex: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '${item.speed.toStringAsFixed(3)} m/s', - style: const TextStyle( - fontSize: 13, fontWeight: FontWeight.bold), - ), - Text( - '${kmh.toStringAsFixed(2)} km/h', - style: TextStyle(fontSize: 11, color: Colors.grey.shade400), - ), - ], + // ── Baris kedua: total + pulsa + sampel ─────────── + if (hasTotal) ...[ + const SizedBox(height: 6), + Padding( + padding: const EdgeInsets.only(left: 28), + child: Wrap( + spacing: 6, + runSpacing: 4, + children: [ + _StatChip( + icon: Icons.route_rounded, + label: '${item.totalWindwegKm.toStringAsFixed(3)} km total', + tooltip: 'Total jarak angin periode ini', + 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 // ════════════════════════════════════════════════════════════ diff --git a/packages/monitoring_repository/lib/src/models/wind_speed.dart b/packages/monitoring_repository/lib/src/models/wind_speed.dart index 4bfe291..2bb90e3 100644 --- a/packages/monitoring_repository/lib/src/models/wind_speed.dart +++ b/packages/monitoring_repository/lib/src/models/wind_speed.dart @@ -1,12 +1,16 @@ class MyWindSpeed { final double windwegKm; + final double totalWindwegKm; final double maxWindwegKm; + final int totalPulse; final int sampleCount; final DateTime timestamp; MyWindSpeed({ required this.windwegKm, + this.totalWindwegKm = 0.0, this.maxWindwegKm = 0.0, + this.totalPulse = 0, this.sampleCount = 0, required this.timestamp, }); @@ -22,9 +26,6 @@ class MyWindSpeed { factory MyWindSpeed.fromJson(Map json) { return MyWindSpeed( - // realtime → windweg_km - // history → avg_windweg_km - // average → windweg_km windwegKm: ((json['windweg_km'] ?? json['avg_windweg_km'] ?? @@ -32,7 +33,9 @@ class MyWindSpeed { json['speed'] ?? 0)) .toDouble(), + totalWindwegKm: (json['total_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, timestamp: DateTime.fromMillisecondsSinceEpoch( (json['timestamp'] ?? 0) * 1000,