Merge branch 'branch' of https://github.com/kleponijo/klimatologi into branch

This commit is contained in:
Mochamad ongki ramadani 2026-05-28 22:33:34 +07:00
commit 1178fc335d
6 changed files with 153 additions and 42 deletions

View File

@ -32,6 +32,8 @@ class DeviceSetupBloc extends Bloc<DeviceSetupEvent, DeviceSetupState> {
(e, emit) => emit(state.copyWith(intervalRealtimeMs: e.ms))); (e, emit) => emit(state.copyWith(intervalRealtimeMs: e.ms)));
on<IntervalHistoryChanged>( on<IntervalHistoryChanged>(
(e, emit) => emit(state.copyWith(intervalHistoryMs: e.ms))); (e, emit) => emit(state.copyWith(intervalHistoryMs: e.ms)));
on<MagnetCountChanged>(
(e, emit) => emit(state.copyWith(magnetCount: e.count)));
on<DeviceSettingsSaved>(_onSettingsSaved); on<DeviceSettingsSaved>(_onSettingsSaved);
on<DeviceLogsRefreshed>(_onLogsRefreshed); on<DeviceLogsRefreshed>(_onLogsRefreshed);
on<DeviceRestartRequested>(_onRestartRequested); on<DeviceRestartRequested>(_onRestartRequested);
@ -64,6 +66,7 @@ class DeviceSetupBloc extends Bloc<DeviceSetupEvent, DeviceSetupState> {
radiusM: s['radius_m'] as double, radiusM: s['radius_m'] as double,
intervalRealtimeMs: s['interval_realtime_ms'] as int, intervalRealtimeMs: s['interval_realtime_ms'] as int,
intervalHistoryMs: s['interval_history_ms'] as int, intervalHistoryMs: s['interval_history_ms'] as int,
magnetCount: (s['magnet_count'] as int?) ?? 1,
logs: logs, logs: logs,
)); ));
} catch (e) { } catch (e) {
@ -101,6 +104,7 @@ class DeviceSetupBloc extends Bloc<DeviceSetupEvent, DeviceSetupState> {
radiusM: state.radiusM, radiusM: state.radiusM,
intervalRealtimeMs: state.intervalRealtimeMs, intervalRealtimeMs: state.intervalRealtimeMs,
intervalHistoryMs: state.intervalHistoryMs, intervalHistoryMs: state.intervalHistoryMs,
magnetCount: state.magnetCount,
); );
emit(state.copyWith(status: DeviceSetupStatus.settingsSaved)); emit(state.copyWith(status: DeviceSetupStatus.settingsSaved));
await Future.delayed(const Duration(seconds: 2)); await Future.delayed(const Duration(seconds: 2));

View File

@ -2,59 +2,51 @@ part of 'device_setup_bloc.dart';
abstract class DeviceSetupEvent {} abstract class DeviceSetupEvent {}
/// Cek apakah HP sedang terhubung ke hotspot ESP
class CheckEspConnectionEvent extends DeviceSetupEvent {} class CheckEspConnectionEvent extends DeviceSetupEvent {}
/// Kirim SSID + password baru ke ESP via HTTP
class SendWifiCredentialsEvent extends DeviceSetupEvent { class SendWifiCredentialsEvent extends DeviceSetupEvent {
final String ssid; final String ssid;
final String password; final String password;
SendWifiCredentialsEvent({required this.ssid, required this.password}); SendWifiCredentialsEvent({required this.ssid, required this.password});
} }
/// Reset state ke awal (misalnya saat user keluar screen)
class ResetDeviceSetupEvent extends DeviceSetupEvent {} class ResetDeviceSetupEvent extends DeviceSetupEvent {}
// Sensor Settings events (new)
/// Load device ID dari SharedPreferences + settings dari Firebase
class DeviceSettingsStarted extends DeviceSetupEvent {} class DeviceSettingsStarted extends DeviceSetupEvent {}
/// User pilih/ubah device ID (misal: esp_percobaan esp_lapangan)
class DeviceIdChanged extends DeviceSetupEvent { class DeviceIdChanged extends DeviceSetupEvent {
final String deviceId; final String deviceId;
DeviceIdChanged(this.deviceId); DeviceIdChanged(this.deviceId);
} }
/// User ubah k_faktor di form
class KFaktorChanged extends DeviceSetupEvent { class KFaktorChanged extends DeviceSetupEvent {
final double value; final double value;
KFaktorChanged(this.value); KFaktorChanged(this.value);
} }
/// User ubah radius_m di form
class RadiusChanged extends DeviceSetupEvent { class RadiusChanged extends DeviceSetupEvent {
final double value; final double value;
RadiusChanged(this.value); RadiusChanged(this.value);
} }
/// User ubah interval realtime (ms)
class IntervalRealtimeChanged extends DeviceSetupEvent { class IntervalRealtimeChanged extends DeviceSetupEvent {
final int ms; final int ms;
IntervalRealtimeChanged(this.ms); IntervalRealtimeChanged(this.ms);
} }
/// User ubah interval history (ms)
class IntervalHistoryChanged extends DeviceSetupEvent { class IntervalHistoryChanged extends DeviceSetupEvent {
final int ms; final int ms;
IntervalHistoryChanged(this.ms); IntervalHistoryChanged(this.ms);
} }
/// Simpan settings ke Firebase /// User ubah jumlah magnet (1 atau 3)
class MagnetCountChanged extends DeviceSetupEvent {
final int count;
MagnetCountChanged(this.count);
}
class DeviceSettingsSaved extends DeviceSetupEvent {} class DeviceSettingsSaved extends DeviceSetupEvent {}
/// Refresh logs dari Firebase
class DeviceLogsRefreshed extends DeviceSetupEvent {} class DeviceLogsRefreshed extends DeviceSetupEvent {}
class DeviceRestartRequested extends DeviceSetupEvent {} class DeviceRestartRequested extends DeviceSetupEvent {}

View File

@ -1,14 +1,13 @@
part of 'device_setup_bloc.dart'; part of 'device_setup_bloc.dart';
enum DeviceSetupStatus { enum DeviceSetupStatus {
idle, // belum ada aksi idle,
checkingConn, // sedang cek koneksi ke ESP checkingConn,
notConnected, // HP belum konek ke hotspot ESP notConnected,
connected, // HP sudah konek ke hotspot ESP, siap kirim connected,
sending, // sedang kirim SSID+password ke ESP sending,
success, // ESP berhasil terima & akan restart success,
failure, // gagal (timeout, ESP tidak respond, dll) failure,
// Settings
settingsLoading, settingsLoading,
settingsLoaded, settingsLoaded,
settingsSaving, settingsSaving,
@ -21,25 +20,17 @@ class DeviceSetupState {
final String? errorMessage; final String? errorMessage;
final String? successMessage; final String? successMessage;
final String espIp; final String espIp;
// Sensor settings
/// ID device aktif menentukan path Firebase yang dibaca app.
/// Default 'esp_lapangan' (ESP utama di lapangan).
/// Disimpan lokal di SharedPreferences.
final String deviceId; final String deviceId;
/// Konstanta kalibrasi. Default 50.0 (hasil estimasi vs AWS).
final double kFaktor; final double kFaktor;
/// Jari-jari lengan anemometer dalam meter. Default 0.08 (8 cm).
final double radiusM; final double radiusM;
/// Interval pengiriman realtime ke Firebase (ms). Default 1000 = 1 detik.
final int intervalRealtimeMs; final int intervalRealtimeMs;
/// Interval push history ke Firebase (ms). Default 3600000 = 1 jam.
final int intervalHistoryMs; final int intervalHistoryMs;
// Logs /// Jumlah magnet pada anemometer. 1 = default, 3 = resolusi lebih tinggi.
/// Disimpan di Firebase: /anemometer/settings/magnet_count
final int magnetCount;
final List<Map<String, dynamic>> logs; final List<Map<String, dynamic>> logs;
final bool logsLoading; final bool logsLoading;
@ -47,14 +38,13 @@ class DeviceSetupState {
this.status = DeviceSetupStatus.idle, this.status = DeviceSetupStatus.idle,
this.errorMessage, this.errorMessage,
this.successMessage, this.successMessage,
this.espIp = '192.168.4.1', // default IP ESP saat AP mode this.espIp = '192.168.4.1',
// Settings default sama dengan cfg_config.h di ESP
this.deviceId = 'esp_lapangan', this.deviceId = 'esp_lapangan',
this.kFaktor = 50.0, this.kFaktor = 50.0,
this.radiusM = 0.08, this.radiusM = 0.08,
this.intervalRealtimeMs = 1000, this.intervalRealtimeMs = 1000,
this.intervalHistoryMs = 3600000, this.intervalHistoryMs = 3600000,
// Logs this.magnetCount = 1, // default 1 magnet
this.logs = const [], this.logs = const [],
this.logsLoading = false, this.logsLoading = false,
}); });
@ -69,6 +59,7 @@ class DeviceSetupState {
double? radiusM, double? radiusM,
int? intervalRealtimeMs, int? intervalRealtimeMs,
int? intervalHistoryMs, int? intervalHistoryMs,
int? magnetCount,
List<Map<String, dynamic>>? logs, List<Map<String, dynamic>>? logs,
bool? logsLoading, bool? logsLoading,
}) { }) {
@ -82,6 +73,7 @@ class DeviceSetupState {
radiusM: radiusM ?? this.radiusM, radiusM: radiusM ?? this.radiusM,
intervalRealtimeMs: intervalRealtimeMs ?? this.intervalRealtimeMs, intervalRealtimeMs: intervalRealtimeMs ?? this.intervalRealtimeMs,
intervalHistoryMs: intervalHistoryMs ?? this.intervalHistoryMs, intervalHistoryMs: intervalHistoryMs ?? this.intervalHistoryMs,
magnetCount: magnetCount ?? this.magnetCount,
logs: logs ?? this.logs, logs: logs ?? this.logs,
logsLoading: logsLoading ?? this.logsLoading, logsLoading: logsLoading ?? this.logsLoading,
); );

View File

@ -289,6 +289,21 @@ class _SensorSettingsTab extends StatelessWidget {
]), ]),
const SizedBox(height: 24), const SizedBox(height: 24),
// Jumlah Magnet
_sectionTitle('Jumlah Magnet'),
const SizedBox(height: 4),
Text(
'Sesuaikan dengan jumlah magnet yang terpasang di lengan anemometer. '
'Lebih banyak magnet = resolusi lebih tinggi.',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
const SizedBox(height: 10),
_MagnetCountSelector(
selected: state.magnetCount,
onSelected: (count) => bloc.add(MagnetCountChanged(count)),
),
const SizedBox(height: 24),
// Interval Realtime // Interval Realtime
_sectionTitle('Interval Pengiriman Realtime'), _sectionTitle('Interval Pengiriman Realtime'),
const SizedBox(height: 4), const SizedBox(height: 4),
@ -325,6 +340,7 @@ class _SensorSettingsTab extends StatelessWidget {
_FormulaPreview( _FormulaPreview(
kFaktor: state.kFaktor, kFaktor: state.kFaktor,
radiusM: state.radiusM, radiusM: state.radiusM,
magnetCount: state.magnetCount,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
@ -879,12 +895,18 @@ class _SettingsCard extends StatelessWidget {
class _FormulaPreview extends StatelessWidget { class _FormulaPreview extends StatelessWidget {
final double kFaktor; final double kFaktor;
final double radiusM; final double radiusM;
const _FormulaPreview({required this.kFaktor, required this.radiusM}); final int magnetCount;
const _FormulaPreview({
required this.kFaktor,
required this.radiusM,
required this.magnetCount,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Contoh: 1 pulsa per detik // Contoh: 1 pulsa per detik
final exampleRps = 1.0; final exampleRps = 1.0 / magnetCount;
final exampleSpeed = 2 * 3.14159 * radiusM * exampleRps * kFaktor; final exampleSpeed = 2 * 3.14159 * radiusM * exampleRps * kFaktor;
return Container( return Container(
@ -908,7 +930,7 @@ class _FormulaPreview extends StatelessWidget {
]), ]),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'speed = 2π × radius × RPS × k_faktor', 'speed = 2π × radius × (pulsa / (dt × magnet)) × k',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey.shade400, color: Colors.grey.shade400,
@ -916,7 +938,7 @@ class _FormulaPreview extends StatelessWidget {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
' = 2π × ${radiusM.toStringAsFixed(3)}m × RPS × ${kFaktor.toStringAsFixed(1)}', ' = 2π × ${radiusM.toStringAsFixed(3)}m × (p / (dt × $magnetCount)) × ${kFaktor.toStringAsFixed(1)}',
style: const TextStyle( style: const TextStyle(
fontSize: 12, color: Colors.white70, fontFamily: 'monospace'), fontSize: 12, color: Colors.white70, fontFamily: 'monospace'),
), ),
@ -935,6 +957,105 @@ class _FormulaPreview extends StatelessWidget {
} }
} }
// WIDGET BARU: _MagnetCountSelector
// Tambahkan class baru ini di bagian bawah file,
// setelah class _FormulaPreview
class _MagnetCountSelector extends StatelessWidget {
final int selected;
final ValueChanged<int> onSelected;
const _MagnetCountSelector({
required this.selected,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
const options = [1, 2, 3];
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 6),
],
),
child: Row(
children: options.map((count) {
final isSelected = count == selected;
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: InkWell(
onTap: () => onSelected(count),
borderRadius: BorderRadius.circular(10),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: isSelected
? Colors.blue.shade700
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected
? Colors.blue.shade700
: Colors.grey.shade300,
),
),
child: Column(
children: [
// Gambar titik magnet
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
count,
(_) => Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected
? Colors.white
: Colors.blue.shade400,
),
),
),
),
const SizedBox(height: 6),
Text(
'$count magnet',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.white : Colors.black87,
),
),
Text(
count == 1 ? 'default' : 'resolusi ${count}×',
style: TextStyle(
fontSize: 10,
color: isSelected
? Colors.white70
: Colors.grey.shade500,
),
),
],
),
),
),
),
);
}).toList(),
),
);
}
}
// //
// Widgets internal // Widgets internal
// //

View File

@ -98,6 +98,7 @@ class FirebaseMonitoringRepo implements MonitoringRepository {
double? radiusM, double? radiusM,
int? intervalRealtimeMs, int? intervalRealtimeMs,
int? intervalHistoryMs, int? intervalHistoryMs,
int? magnetCount,
}) async { }) async {
final updates = <String, dynamic>{}; final updates = <String, dynamic>{};
if (kFaktor != null) updates['k_faktor'] = kFaktor; if (kFaktor != null) updates['k_faktor'] = kFaktor;

View File

@ -30,6 +30,7 @@ abstract class MonitoringRepository {
double? radiusM, double? radiusM,
int? intervalRealtimeMs, int? intervalRealtimeMs,
int? intervalHistoryMs, int? intervalHistoryMs,
int? magnetCount,
}); });
// Device Logs // Device Logs