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

View File

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

View File

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

View File

@ -289,6 +289,21 @@ class _SensorSettingsTab extends StatelessWidget {
]),
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
_sectionTitle('Interval Pengiriman Realtime'),
const SizedBox(height: 4),
@ -325,6 +340,7 @@ class _SensorSettingsTab extends StatelessWidget {
_FormulaPreview(
kFaktor: state.kFaktor,
radiusM: state.radiusM,
magnetCount: state.magnetCount,
),
const SizedBox(height: 32),
@ -879,12 +895,18 @@ class _SettingsCard extends StatelessWidget {
class _FormulaPreview extends StatelessWidget {
final double kFaktor;
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
Widget build(BuildContext context) {
// Contoh: 1 pulsa per detik
final exampleRps = 1.0;
final exampleRps = 1.0 / magnetCount;
final exampleSpeed = 2 * 3.14159 * radiusM * exampleRps * kFaktor;
return Container(
@ -908,7 +930,7 @@ class _FormulaPreview extends StatelessWidget {
]),
const SizedBox(height: 10),
Text(
'speed = 2π × radius × RPS × k_faktor',
'speed = 2π × radius × (pulsa / (dt × magnet)) × k',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade400,
@ -916,7 +938,7 @@ class _FormulaPreview extends StatelessWidget {
),
const SizedBox(height: 6),
Text(
' = 2π × ${radiusM.toStringAsFixed(3)}m × RPS × ${kFaktor.toStringAsFixed(1)}',
' = 2π × ${radiusM.toStringAsFixed(3)}m × (p / (dt × $magnetCount)) × ${kFaktor.toStringAsFixed(1)}',
style: const TextStyle(
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
//

View File

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

View File

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