This commit is contained in:
kleponijo 2026-06-29 11:51:24 +07:00
parent 4fd56dc5f9
commit 74a1609c21
7 changed files with 70 additions and 172 deletions

View File

@ -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);

View File

@ -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,

View File

@ -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,
));
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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,

View File

@ -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);
}