oke
This commit is contained in:
parent
4fd56dc5f9
commit
74a1609c21
|
|
@ -19,27 +19,21 @@ class DeviceIdChanged extends DeviceSetupEvent {
|
|||
DeviceIdChanged(this.deviceId);
|
||||
}
|
||||
|
||||
class KFaktorChanged extends DeviceSetupEvent {
|
||||
final double value;
|
||||
KFaktorChanged(this.value);
|
||||
}
|
||||
|
||||
class RadiusChanged extends DeviceSetupEvent {
|
||||
final double value;
|
||||
RadiusChanged(this.value);
|
||||
}
|
||||
|
||||
class IntervalRealtimeChanged extends DeviceSetupEvent {
|
||||
final int ms;
|
||||
IntervalRealtimeChanged(this.ms);
|
||||
}
|
||||
|
||||
class IntervalAverageChanged extends DeviceSetupEvent {
|
||||
final int ms;
|
||||
IntervalAverageChanged(this.ms);
|
||||
}
|
||||
|
||||
class IntervalHistoryChanged extends DeviceSetupEvent {
|
||||
final int ms;
|
||||
IntervalHistoryChanged(this.ms);
|
||||
}
|
||||
|
||||
/// User ubah jumlah magnet (1 atau 3)
|
||||
class MagnetCountChanged extends DeviceSetupEvent {
|
||||
final int count;
|
||||
MagnetCountChanged(this.count);
|
||||
|
|
|
|||
|
|
@ -22,9 +22,8 @@ class DeviceSetupState {
|
|||
final String espIp;
|
||||
|
||||
final String deviceId;
|
||||
final double kFaktor;
|
||||
final double radiusM;
|
||||
final int intervalRealtimeMs;
|
||||
final int intervalAverageMs;
|
||||
final int intervalHistoryMs;
|
||||
final int magnetCount;
|
||||
final List<Map<String, dynamic>> logs;
|
||||
|
|
@ -38,9 +37,8 @@ class DeviceSetupState {
|
|||
this.successMessage,
|
||||
this.espIp = '192.168.4.1',
|
||||
this.deviceId = 'esp_lapangan',
|
||||
this.kFaktor = 50.0,
|
||||
this.radiusM = 0.08,
|
||||
this.intervalRealtimeMs = 1000,
|
||||
this.intervalAverageMs = 60000,
|
||||
this.intervalHistoryMs = 3600000,
|
||||
this.magnetCount = 1, // ← default 1 magnet
|
||||
this.logs = const [],
|
||||
|
|
@ -58,9 +56,8 @@ class DeviceSetupState {
|
|||
String? successMessage,
|
||||
String? espIp,
|
||||
String? deviceId,
|
||||
double? kFaktor,
|
||||
double? radiusM,
|
||||
int? intervalRealtimeMs,
|
||||
int? intervalAverageMs,
|
||||
int? intervalHistoryMs,
|
||||
int? magnetCount,
|
||||
List<Map<String, dynamic>>? logs,
|
||||
|
|
@ -74,9 +71,8 @@ class DeviceSetupState {
|
|||
successMessage: successMessage,
|
||||
espIp: espIp ?? this.espIp,
|
||||
deviceId: deviceId ?? this.deviceId,
|
||||
kFaktor: kFaktor ?? this.kFaktor,
|
||||
radiusM: radiusM ?? this.radiusM,
|
||||
intervalRealtimeMs: intervalRealtimeMs ?? this.intervalRealtimeMs,
|
||||
intervalAverageMs: intervalAverageMs ?? this.intervalAverageMs,
|
||||
intervalHistoryMs: intervalHistoryMs ?? this.intervalHistoryMs,
|
||||
magnetCount: magnetCount ?? this.magnetCount,
|
||||
logs: logs ?? this.logs,
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ class DeviceSetupBloc extends Bloc<DeviceSetupEvent, DeviceSetupState> {
|
|||
// Settings
|
||||
on<DeviceSettingsStarted>(_onSettingsStarted);
|
||||
on<DeviceIdChanged>(_onDeviceIdChanged);
|
||||
on<KFaktorChanged>((e, emit) => emit(state.copyWith(kFaktor: e.value)));
|
||||
on<RadiusChanged>((e, emit) => emit(state.copyWith(radiusM: e.value)));
|
||||
on<IntervalRealtimeChanged>(
|
||||
(e, emit) => emit(state.copyWith(intervalRealtimeMs: e.ms)));
|
||||
on<IntervalAverageChanged>(
|
||||
(e, emit) => emit(state.copyWith(intervalAverageMs: e.ms)));
|
||||
on<IntervalHistoryChanged>(
|
||||
(e, emit) => emit(state.copyWith(intervalHistoryMs: e.ms)));
|
||||
on<MagnetCountChanged>(
|
||||
|
|
@ -56,19 +56,14 @@ class DeviceSetupBloc extends Bloc<DeviceSetupEvent, DeviceSetupState> {
|
|||
// Baca device ID tersimpan lokal
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedId = prefs.getString(_kDeviceIdKey) ?? state.deviceId;
|
||||
|
||||
// Baca settings dari Firebase
|
||||
final s = await _repository.getAnemometerSettings();
|
||||
|
||||
// Baca logs
|
||||
final logs = await _repository.getDeviceLogs(savedId);
|
||||
|
||||
emit(state.copyWith(
|
||||
status: DeviceSetupStatus.settingsLoaded,
|
||||
deviceId: savedId,
|
||||
kFaktor: s['k_faktor'] as double,
|
||||
radiusM: s['radius_m'] as double,
|
||||
intervalRealtimeMs: s['interval_realtime_ms'] as int,
|
||||
intervalAverageMs: s['interval_average_ms'] as int,
|
||||
intervalHistoryMs: s['interval_history_ms'] as int,
|
||||
magnetCount: (s['magnet_count'] as int?) ?? 1,
|
||||
logs: logs,
|
||||
|
|
@ -104,9 +99,8 @@ class DeviceSetupBloc extends Bloc<DeviceSetupEvent, DeviceSetupState> {
|
|||
emit(state.copyWith(status: DeviceSetupStatus.settingsSaving));
|
||||
try {
|
||||
await _repository.updateAnemometerSettings(
|
||||
kFaktor: state.kFaktor,
|
||||
radiusM: state.radiusM,
|
||||
intervalRealtimeMs: state.intervalRealtimeMs,
|
||||
intervalAverageMs: state.intervalAverageMs,
|
||||
intervalHistoryMs: state.intervalHistoryMs,
|
||||
magnetCount: state.magnetCount,
|
||||
);
|
||||
|
|
@ -335,9 +329,8 @@ class DeviceSetupBloc extends Bloc<DeviceSetupEvent, DeviceSetupState> {
|
|||
) {
|
||||
emit(DeviceSetupState(
|
||||
deviceId: state.deviceId,
|
||||
kFaktor: state.kFaktor,
|
||||
radiusM: state.radiusM,
|
||||
intervalRealtimeMs: state.intervalRealtimeMs,
|
||||
intervalAverageMs: state.intervalAverageMs,
|
||||
intervalHistoryMs: state.intervalHistoryMs,
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@ const _kRealtimeOptions = <String, int>{
|
|||
'1 jam': 3600000,
|
||||
};
|
||||
|
||||
const _kAverageOptions = <String, int>{
|
||||
'30 detik': 30000,
|
||||
'1 menit': 60000,
|
||||
'5 menit': 300000,
|
||||
'10 menit': 600000,
|
||||
'30 menit': 1800000,
|
||||
};
|
||||
|
||||
// ── Opsi interval history ─────────────────────────────────────
|
||||
const _kHistoryOptions = <String, int>{
|
||||
'10 menit': 600000,
|
||||
|
|
@ -45,8 +53,6 @@ class _DeviceSetupScreenState extends State<DeviceSetupScreen>
|
|||
bool _obscurePassword = true;
|
||||
|
||||
// Settings form controllers
|
||||
final _kFaktorController = TextEditingController();
|
||||
final _radiusController = TextEditingController();
|
||||
String? _customDeviceId;
|
||||
|
||||
@override
|
||||
|
|
@ -60,20 +66,11 @@ class _DeviceSetupScreenState extends State<DeviceSetupScreen>
|
|||
_tabController.dispose();
|
||||
_ssidController.dispose();
|
||||
_passwordController.dispose();
|
||||
_kFaktorController.dispose();
|
||||
_radiusController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ── Sync text controllers dengan state saat settings loaded ──
|
||||
void _syncControllers(DeviceSetupState state) {
|
||||
if (_kFaktorController.text != state.kFaktor.toString()) {
|
||||
_kFaktorController.text = state.kFaktor.toStringAsFixed(2);
|
||||
}
|
||||
if (_radiusController.text != state.radiusM.toString()) {
|
||||
_radiusController.text = state.radiusM.toStringAsFixed(3);
|
||||
}
|
||||
}
|
||||
void _syncControllers(DeviceSetupState state) {}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -144,8 +141,6 @@ class _DeviceSetupScreenState extends State<DeviceSetupScreen>
|
|||
children: [
|
||||
_SensorSettingsTab(
|
||||
state: state,
|
||||
kFaktorController: _kFaktorController,
|
||||
radiusController: _radiusController,
|
||||
customDeviceId: _customDeviceId,
|
||||
onCustomDeviceIdChanged: (v) =>
|
||||
setState(() => _customDeviceId = v),
|
||||
|
|
@ -215,15 +210,11 @@ class _DeviceSetupScreenState extends State<DeviceSetupScreen>
|
|||
// ═══════════════════════════════════════════════════════════════
|
||||
class _SensorSettingsTab extends StatelessWidget {
|
||||
final DeviceSetupState state;
|
||||
final TextEditingController kFaktorController;
|
||||
final TextEditingController radiusController;
|
||||
final String? customDeviceId;
|
||||
final ValueChanged<String?> onCustomDeviceIdChanged;
|
||||
|
||||
const _SensorSettingsTab({
|
||||
required this.state,
|
||||
required this.kFaktorController,
|
||||
required this.radiusController,
|
||||
required this.customDeviceId,
|
||||
required this.onCustomDeviceIdChanged,
|
||||
});
|
||||
|
|
@ -259,36 +250,6 @@ class _SensorSettingsTab extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Kalibrasi ────────────────────────────────────────
|
||||
_sectionTitle('Kalibrasi'),
|
||||
const SizedBox(height: 10),
|
||||
_SettingsCard(children: [
|
||||
_NumericField(
|
||||
controller: kFaktorController,
|
||||
label: 'K-Faktor',
|
||||
hint: 'Contoh: 50.0',
|
||||
suffix: '×',
|
||||
helper: 'Konstanta kalibrasi vs AWS. Awal: 50.0',
|
||||
onChanged: (v) {
|
||||
final d = double.tryParse(v);
|
||||
if (d != null && d > 0) bloc.add(KFaktorChanged(d));
|
||||
},
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_NumericField(
|
||||
controller: radiusController,
|
||||
label: 'Jari-jari Lengan',
|
||||
hint: 'Contoh: 0.08',
|
||||
suffix: 'm',
|
||||
helper: 'Jarak pusat ke ujung cup anemometer. Default: 0.08 m',
|
||||
onChanged: (v) {
|
||||
final d = double.tryParse(v);
|
||||
if (d != null && d > 0) bloc.add(RadiusChanged(d));
|
||||
},
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Jumlah Magnet ─────────────────────────────────────
|
||||
_sectionTitle('Jumlah Magnet'),
|
||||
const SizedBox(height: 4),
|
||||
|
|
@ -320,6 +281,21 @@ class _SensorSettingsTab extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
_sectionTitle('Interval Rata-rata (Average)'),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Seberapa sering data rata-rata dikirim ke Firebase. '
|
||||
'Harus lebih pendek dari interval history.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_ChipSelector(
|
||||
options: _kAverageOptions,
|
||||
selectedMs: state.intervalAverageMs,
|
||||
onSelected: (ms) => bloc.add(IntervalAverageChanged(ms)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Interval History ──────────────────────────────────
|
||||
_sectionTitle('Interval History (Rata-rata Jangka Panjang)'),
|
||||
const SizedBox(height: 4),
|
||||
|
|
@ -969,66 +945,6 @@ class _ChipSelector extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _NumericField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final String label;
|
||||
final String hint;
|
||||
final String suffix;
|
||||
final String helper;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
const _NumericField({
|
||||
required this.controller,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
required this.suffix,
|
||||
required this.helper,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.]'))],
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
suffixText: suffix,
|
||||
helperText: helper,
|
||||
helperMaxLines: 2,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsCard extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
const _SettingsCard({required this.children});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start, children: children),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MagnetCountSelector extends StatelessWidget {
|
||||
final int selected;
|
||||
final ValueChanged<int> onSelected;
|
||||
|
|
|
|||
|
|
@ -103,46 +103,49 @@ class FirebaseMonitoringRepo implements MonitoringRepository {
|
|||
Future<Map<String, dynamic>> getAnemometerSettings() async {
|
||||
// Default sama dengan cfg_config.h di ESP
|
||||
const defaults = <String, dynamic>{
|
||||
'k_faktor': 50.0,
|
||||
'radius_m': 0.08,
|
||||
'interval_realtime_ms': 1000,
|
||||
'interval_average_ms': 60000,
|
||||
'interval_history_ms': 3600000,
|
||||
'magnet_count': 3,
|
||||
};
|
||||
try {
|
||||
final snapshot = await _db.ref('anemometer/settings').get();
|
||||
if (snapshot.exists && snapshot.value is Map) {
|
||||
final raw = snapshot.value as Map<dynamic, dynamic>;
|
||||
return {
|
||||
'k_faktor': (raw['k_faktor'] ?? defaults['k_faktor']).toDouble(),
|
||||
'radius_m': (raw['radius_m'] ?? defaults['radius_m']).toDouble(),
|
||||
'interval_realtime_ms':
|
||||
(raw['interval_realtime_ms'] ?? defaults['interval_realtime_ms'])
|
||||
as int,
|
||||
'interval_average_ms':
|
||||
(raw['interval_average_ms'] ?? defaults['interval_average_ms'])
|
||||
as int,
|
||||
'interval_history_ms':
|
||||
(raw['interval_history_ms'] ?? defaults['interval_history_ms'])
|
||||
as int,
|
||||
'magnet_count':
|
||||
(raw['magnet_count'] as int? ?? defaults['magnet_count']) as int,
|
||||
};
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
// ── Tulis settings ke Firebase (dari app) ────────────────────
|
||||
@override
|
||||
Future<void> updateAnemometerSettings({
|
||||
double? kFaktor,
|
||||
double? radiusM,
|
||||
int? intervalRealtimeMs,
|
||||
int? intervalAverageMs,
|
||||
int? intervalHistoryMs,
|
||||
int? magnetCount,
|
||||
}) async {
|
||||
final updates = <String, dynamic>{};
|
||||
if (kFaktor != null) updates['k_faktor'] = kFaktor;
|
||||
if (intervalRealtimeMs != null)
|
||||
updates['interval_realtime_ms'] = intervalRealtimeMs;
|
||||
if (intervalAverageMs != null)
|
||||
updates['interval_average_ms'] = intervalAverageMs;
|
||||
if (intervalHistoryMs != null)
|
||||
updates['interval_history_ms'] = intervalHistoryMs;
|
||||
if (magnetCount != null) updates['magnet_count'] = magnetCount;
|
||||
if (updates.isNotEmpty) {
|
||||
await _db.ref('anemometer/settings').update(updates);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,38 @@
|
|||
class MyWindSpeed {
|
||||
final double speed; // realtime: speed_ms | history: avg_ms
|
||||
final double maxSpeed; // history only (0 jika dari realtime)
|
||||
final double kFaktor;
|
||||
final double windwegKm;
|
||||
final double maxWindwegKm;
|
||||
final int sampleCount;
|
||||
final DateTime timestamp;
|
||||
|
||||
MyWindSpeed({
|
||||
required this.speed,
|
||||
this.maxSpeed = 0.0,
|
||||
this.kFaktor = 1.0,
|
||||
required this.windwegKm,
|
||||
this.maxWindwegKm = 0.0,
|
||||
this.sampleCount = 0,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
// Getter alias untuk kompatibilitas dengan bloc/chart yang pakai .speed
|
||||
double get speed => windwegKm;
|
||||
double get maxSpeed => maxWindwegKm;
|
||||
|
||||
static final empty = MyWindSpeed(
|
||||
speed: 0.0,
|
||||
windwegKm: 0.0,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
|
||||
// Dipakai untuk realtime node
|
||||
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
|
||||
return MyWindSpeed(
|
||||
// support field lama 'speed' dan baru 'speed_ms'/'avg_ms'
|
||||
speed: ((json['speed_ms'] ?? json['avg_ms'] ?? json['speed']) ?? 0)
|
||||
.toDouble(),
|
||||
maxSpeed: (json['max_ms'] ?? 0).toDouble(),
|
||||
kFaktor: (json['k_faktor'] ?? 1.0).toDouble(),
|
||||
// realtime → windweg_km
|
||||
// history → avg_windweg_km
|
||||
// average → windweg_km
|
||||
windwegKm:
|
||||
((json['windweg_km'] ??
|
||||
json['avg_windweg_km'] ??
|
||||
json['speed_ms'] ?? // fallback data lama
|
||||
json['speed'] ??
|
||||
0))
|
||||
.toDouble(),
|
||||
maxWindwegKm: (json['max_windweg_km'] ?? 0).toDouble(),
|
||||
sampleCount: (json['sample_count'] ?? 0) as int,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
(json['timestamp'] ?? 0) * 1000,
|
||||
|
|
|
|||
|
|
@ -17,18 +17,11 @@ abstract class MonitoringRepository {
|
|||
T Function(Map<dynamic, dynamic> json) mapper,
|
||||
);
|
||||
|
||||
// ── Anemometer Settings ──────────────────────────────────────
|
||||
/// Baca semua settings dari /anemometer/settings/
|
||||
/// Return map dengan keys:
|
||||
/// k_faktor (double), radius_m (double),
|
||||
/// interval_realtime_ms (int), interval_history_ms (int)
|
||||
Future<Map<String, dynamic>> getAnemometerSettings();
|
||||
|
||||
/// Tulis settings ke /anemometer/settings/ (field yang null tidak ditulis)
|
||||
Future<void> updateAnemometerSettings({
|
||||
double? kFaktor,
|
||||
double? radiusM,
|
||||
int? intervalRealtimeMs,
|
||||
int? intervalAverageMs,
|
||||
int? intervalHistoryMs,
|
||||
int? magnetCount,
|
||||
});
|
||||
|
|
@ -41,15 +34,11 @@ abstract class MonitoringRepository {
|
|||
});
|
||||
|
||||
Future<void> sendRemoteRestart(String deviceId);
|
||||
|
||||
Future<void> deleteDeviceLogs(String deviceId, List<String> keys);
|
||||
|
||||
Future<Map<String, T>> getSensorHistoryWithKeys<T>(
|
||||
String path,
|
||||
T Function(Map<dynamic, dynamic> json) mapper,
|
||||
);
|
||||
|
||||
Future<void> deleteHistoryByKeys(String path, List<String> keys);
|
||||
|
||||
Future<void> deleteAllHistory(String path);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue