overheat monitor with auto poweroff
- needs passwordless sudo on target machine or deploy to run as root
This commit is contained in:
69
pi/ui/lib/services/config_service.dart
Normal file
69
pi/ui/lib/services/config_service.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
/// Configuration service - loads and caches values from config.json
|
||||
///
|
||||
/// Uses singleton pattern. Falls back to defaults if config file missing.
|
||||
class ConfigService {
|
||||
ConfigService._();
|
||||
static final instance = ConfigService._();
|
||||
|
||||
// Loaded config cache
|
||||
Map<String, dynamic>? _config;
|
||||
bool _loaded = false;
|
||||
|
||||
// Defaults
|
||||
static const double _defaultThreshold = 80.0;
|
||||
static const int _defaultTriggerDuration = 10;
|
||||
static const int _defaultShutdownDelay = 10;
|
||||
|
||||
/// Load config from JSON file
|
||||
///
|
||||
/// Looks for config.json in same directory as executable.
|
||||
/// Safe to call multiple times - only loads once.
|
||||
Future<void> load() async {
|
||||
if (_loaded) return;
|
||||
|
||||
try {
|
||||
// Config file sits next to the executable
|
||||
final exePath = Platform.resolvedExecutable;
|
||||
final exeDir = File(exePath).parent.path;
|
||||
final configPath = '$exeDir${Platform.pathSeparator}config.json';
|
||||
|
||||
final file = File(configPath);
|
||||
if (await file.exists()) {
|
||||
final content = await file.readAsString();
|
||||
_config = jsonDecode(content) as Map<String, dynamic>;
|
||||
}
|
||||
} catch (e) {
|
||||
// Config parse error - fall back to defaults
|
||||
_config = null;
|
||||
}
|
||||
|
||||
_loaded = true;
|
||||
}
|
||||
|
||||
/// CPU temperature threshold in Celsius
|
||||
double get overheatThreshold {
|
||||
final overheat = _config?['overheat'] as Map<String, dynamic>?;
|
||||
final value = overheat?['threshold_celsius'];
|
||||
if (value is num) return value.toDouble();
|
||||
return _defaultThreshold;
|
||||
}
|
||||
|
||||
/// How long temp must exceed threshold before triggering
|
||||
Duration get overheatTriggerDuration {
|
||||
final overheat = _config?['overheat'] as Map<String, dynamic>?;
|
||||
final value = overheat?['trigger_duration_sec'];
|
||||
if (value is int) return Duration(seconds: value);
|
||||
return Duration(seconds: _defaultTriggerDuration);
|
||||
}
|
||||
|
||||
/// Countdown before shutdown after overheat triggers
|
||||
Duration get shutdownDelay {
|
||||
final overheat = _config?['overheat'] as Map<String, dynamic>?;
|
||||
final value = overheat?['shutdown_delay_sec'];
|
||||
if (value is int) return Duration(seconds: value);
|
||||
return Duration(seconds: _defaultShutdownDelay);
|
||||
}
|
||||
}
|
||||
77
pi/ui/lib/services/overheat_monitor.dart
Normal file
77
pi/ui/lib/services/overheat_monitor.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'config_service.dart';
|
||||
import 'pi_io.dart';
|
||||
|
||||
/// Monitors CPU temperature and triggers overheat condition
|
||||
///
|
||||
/// Singleton pattern. Polls temp at 500ms intervals to match dashboard.
|
||||
/// When temp exceeds threshold for trigger duration, fires callback.
|
||||
class OverheatMonitor {
|
||||
OverheatMonitor._();
|
||||
static final instance = OverheatMonitor._();
|
||||
|
||||
Timer? _timer;
|
||||
int _consecutiveOverheatSamples = 0;
|
||||
bool _triggered = false;
|
||||
|
||||
static const Duration _pollInterval = Duration(milliseconds: 500);
|
||||
|
||||
/// Current temperature (from PiIO cache)
|
||||
double? get currentTemp => PiIO.instance.getTemperature();
|
||||
|
||||
/// Whether overheat condition has triggered
|
||||
bool get isTriggered => _triggered;
|
||||
|
||||
/// How long we've been over threshold
|
||||
Duration get timeOverThreshold =>
|
||||
Duration(milliseconds: _consecutiveOverheatSamples * _pollInterval.inMilliseconds);
|
||||
|
||||
/// Start monitoring with callback when overheat triggers
|
||||
///
|
||||
/// [onOverheat] fires once when temp exceeds threshold for configured duration.
|
||||
/// Safe to call multiple times - restarts monitoring.
|
||||
void start({required VoidCallback onOverheat}) {
|
||||
stop();
|
||||
_triggered = false;
|
||||
_consecutiveOverheatSamples = 0;
|
||||
|
||||
final threshold = ConfigService.instance.overheatThreshold;
|
||||
final triggerDuration = ConfigService.instance.overheatTriggerDuration;
|
||||
|
||||
_timer = Timer.periodic(_pollInterval, (_) {
|
||||
if (_triggered) return; // Already fired
|
||||
|
||||
final temp = PiIO.instance.getTemperature();
|
||||
if (temp == null) return; // No reading yet
|
||||
|
||||
if (temp > threshold) {
|
||||
_consecutiveOverheatSamples++;
|
||||
|
||||
final overThresholdTime = timeOverThreshold;
|
||||
if (overThresholdTime >= triggerDuration) {
|
||||
_triggered = true;
|
||||
onOverheat();
|
||||
}
|
||||
} else {
|
||||
// Temp dropped below threshold - reset counter
|
||||
_consecutiveOverheatSamples = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Stop monitoring
|
||||
void stop() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
/// Reset state (for testing/recovery)
|
||||
void reset() {
|
||||
_triggered = false;
|
||||
_consecutiveOverheatSamples = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback signature for void functions
|
||||
typedef VoidCallback = void Function();
|
||||
Reference in New Issue
Block a user