-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathefuseCollector.php
More file actions
270 lines (225 loc) · 9.07 KB
/
efuseCollector.php
File metadata and controls
270 lines (225 loc) · 9.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
#!/usr/bin/env php
<?php
/**
* eFuse Metrics Collection Daemon
*
* Runs in the background collecting eFuse current readings every X seconds.
* Writes raw metrics and triggers rollup processing.
*
* Usage: php efuseCollector.php
*
* @package fpp-plugin-watcher
*/
require_once __DIR__ . '/classes/autoload.php'; // Load class autoloader
require_once __DIR__ . '/lib/core/watcherCommon.php';
require_once __DIR__ . '/lib/core/config.php';
use Watcher\Core\Logger;
use Watcher\Core\DaemonLock;
use Watcher\Metrics\EfuseCollector;
use Watcher\Controllers\EfuseHardware;
// Config hot-reload tracking (global for checkAndReloadEfuseConfig)
$lastConfigMtime = file_exists(WATCHERCONFIGFILELOCATION) ? filemtime(WATCHERCONFIGFILELOCATION) : 0;
$config = readPluginConfig();
$collectionInterval = $config['efuseCollectionInterval'] ?? 5;
$retentionDays = $config['efuseRetentionDays'] ?? 14;
function efuseLog($message) {
Logger::getInstance()->info("[eFuse Collector] $message", EFUSE_LOG_FILE);
}
function isEfuseEnabled() {
$config = readPluginConfig(true); // Force refresh
return !empty($config['efuseMonitorEnabled']);
}
function hasEfuseHardware() {
$hardware = EfuseHardware::getInstance()->detectHardware();
return $hardware['supported'];
}
/**
* Check if config file has changed and reload if necessary
* Returns true if config was reloaded, false otherwise
*/
function checkAndReloadEfuseConfig() {
global $config, $lastConfigMtime, $collectionInterval, $retentionDays;
$currentMtime = file_exists(WATCHERCONFIGFILELOCATION) ? filemtime(WATCHERCONFIGFILELOCATION) : 0;
if ($currentMtime <= $lastConfigMtime) {
return false;
}
efuseLog("Config file changed (mtime: $lastConfigMtime -> $currentMtime), reloading configuration...");
$lastConfigMtime = $currentMtime;
// Force reload config (bypass cache)
$newConfig = readPluginConfig(true);
// Check if eFuse monitoring was disabled
if (empty($newConfig['efuseMonitorEnabled'])) {
efuseLog("eFuse monitoring disabled via config reload. Exiting gracefully.");
exit(0);
}
// Log and apply collection interval changes
$newInterval = $newConfig['efuseCollectionInterval'] ?? 5;
if ($newInterval !== $collectionInterval) {
efuseLog("Collection interval changed: {$collectionInterval}s -> {$newInterval}s");
$collectionInterval = $newInterval;
}
// Log and apply retention changes
$newRetention = $newConfig['efuseRetentionDays'] ?? 7;
if ($newRetention !== $retentionDays) {
efuseLog("Retention changed: {$retentionDays} days -> {$newRetention} days");
$retentionDays = $newRetention;
}
// Apply new config
$config = $newConfig;
efuseLog("Configuration reloaded successfully");
return true;
}
/**
* Main collection loop
*/
function runCollector() {
global $collectionInterval, $retentionDays;
efuseLog("eFuse Collector starting...");
// Verify hardware on startup
$hardware = EfuseHardware::getInstance()->detectHardware();
if (!$hardware['supported']) {
efuseLog("ERROR: No compatible eFuse hardware detected. Exiting.");
exit(1);
}
efuseLog("Hardware detected: {$hardware['type']} with {$hardware['ports']} ports");
efuseLog("Collection method: " . ($hardware['details']['method'] ?? 'unknown'));
efuseLog("Collection interval: {$collectionInterval}s, Retention: {$retentionDays} days");
$lastRollupTime = 0;
$lastConfigCheckTime = 0;
$samplesCollected = 0;
$nonZeroSamples = 0;
// Error tracking for resilience
$consecutiveErrors = 0;
$lastErrorLogTime = 0;
$wasInErrorState = false;
while (true) {
$loopStart = microtime(true);
$now = time();
// Check config periodically for hot-reload
try {
if (($now - $lastConfigCheckTime) >= EFUSE_CONFIG_CHECK_INTERVAL) {
$lastConfigCheckTime = $now;
checkAndReloadEfuseConfig();
}
} catch (Exception $e) {
// Config check failed - continue anyway, will retry next interval
}
// Collect eFuse data with error resilience
try {
$reading = EfuseHardware::getInstance()->readEfuseData();
if ($reading['success']) {
// Success - reset error state
if ($wasInErrorState) {
efuseLog("Recovered: fppd connection restored after $consecutiveErrors errors");
$wasInErrorState = false;
}
$consecutiveErrors = 0;
$ports = $reading['ports'];
$samplesCollected++;
// Only write non-zero data (as per plan - skip zeros for storage efficiency)
if (!empty($ports)) {
// Validate readings (cap at max amperage)
foreach ($ports as $portName => $mA) {
if ($mA > EFUSE_MAX_AMPERAGE) {
efuseLog("WARNING: Port $portName reading $mA mA exceeds max (capped at " . EFUSE_MAX_AMPERAGE . ")");
$ports[$portName] = EFUSE_MAX_AMPERAGE;
}
}
if (EfuseCollector::getInstance()->writeRawMetric($ports)) {
$nonZeroSamples++;
}
}
// If all ports are zero, we skip writing (no metric = zero by convention)
} else {
// Read failed - track errors but don't exit
$consecutiveErrors++;
$wasInErrorState = true;
// Only log errors periodically to avoid spam
if (($now - $lastErrorLogTime) >= EFUSE_ERROR_LOG_INTERVAL) {
$lastErrorLogTime = $now;
efuseLog("WARNING: fppd unavailable ($consecutiveErrors consecutive errors): " . ($reading['error'] ?? 'Unknown error'));
}
}
} catch (Exception $e) {
// Catch any unexpected exceptions to prevent daemon crash
$consecutiveErrors++;
$wasInErrorState = true;
if (($now - $lastErrorLogTime) >= EFUSE_ERROR_LOG_INTERVAL) {
$lastErrorLogTime = $now;
efuseLog("ERROR: Exception during collection: " . $e->getMessage());
}
}
// Process rollups periodically (with error handling)
if (($now - $lastRollupTime) >= EFUSE_ROLLUP_INTERVAL) {
$lastRollupTime = $now;
try {
EfuseCollector::getInstance()->processRollup();
} catch (Exception $e) {
efuseLog("ERROR: Rollup processing failed: " . $e->getMessage());
}
// Log status every rollup cycle (if we collected any samples)
if ($samplesCollected > 0) {
$efficiency = round(($nonZeroSamples / $samplesCollected) * 100, 1);
efuseLog("Status: $samplesCollected samples collected, $nonZeroSamples with data ({$efficiency}% non-zero)");
$samplesCollected = 0;
$nonZeroSamples = 0;
} elseif ($wasInErrorState) {
// Log that we're still in error state
efuseLog("Status: fppd unavailable, waiting for recovery...");
}
}
// Calculate sleep time with backoff when errors occur
$elapsed = microtime(true) - $loopStart;
$baseInterval = $collectionInterval;
// Apply exponential backoff when in error state (max 60 seconds)
if ($consecutiveErrors > 0) {
$backoffSeconds = min(EFUSE_MAX_BACKOFF_SECONDS, pow(2, min($consecutiveErrors, 6)));
$baseInterval = $backoffSeconds;
}
$sleepTime = max(0, $baseInterval - $elapsed);
if ($sleepTime > 0) {
usleep(intval($sleepTime * 1000000));
}
}
}
// Signal handling for graceful shutdown
// Use async signals (PHP 7.1+) for immediate signal processing during sleep/blocking calls
if (function_exists('pcntl_async_signals')) {
pcntl_async_signals(true);
}
function signalHandler($signo) {
efuseLog("Received signal $signo, shutting down...");
exit(0);
}
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, 'signalHandler');
pcntl_signal(SIGINT, 'signalHandler');
pcntl_signal(SIGHUP, 'signalHandler');
}
// Acquire daemon lock (handles stale lock detection automatically)
$lockFp = DaemonLock::acquire('efuse-collector', EFUSE_LOG_FILE);
if (!$lockFp) {
exit(1);
}
// Verify enabled before starting
if (!isEfuseEnabled()) {
efuseLog("eFuse monitoring is not enabled in config. Exiting.");
DaemonLock::release($lockFp, 'efuse-collector');
exit(0);
}
// Verify hardware before starting
if (!hasEfuseHardware()) {
efuseLog("No compatible eFuse hardware detected. Exiting.");
DaemonLock::release($lockFp, 'efuse-collector');
exit(0);
}
// Run the collector
try {
runCollector();
} catch (Exception $e) {
efuseLog("FATAL ERROR: " . $e->getMessage());
exit(1);
} finally {
DaemonLock::release($lockFp, 'efuse-collector');
}
?>