Merge branch 'branch' of https://github.com/kleponijo/klimatologi into branch
This commit is contained in:
commit
1178fc335d
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 ──────────────────────────────────────────────
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue