-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmaintain_optimal_voltage.sh
executable file
·419 lines (353 loc) · 14.2 KB
/
maintain_optimal_voltage.sh
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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
#!/bin/bash
# Script to maintain optimal battery voltage by adjusting BCLM dynamically
# Requires jq for parsing JSON outputs
# TODO: Implement logic to adjust bclm based on current/power inputs etc. to optimize charging behavior.
# Check if jq is installed
if ! command -v jq &> /dev/null; then
echo "Error: jq is not installed. Please install jq to continue." >&2
exit 1
fi
# Source the necessary functions
source battery_functions.sh
# Constants
TARGET_VOLTAGE=3.92
DEADBAND=0.02
MIN_BCLM=50
MAX_BCLM=100
# Default values
update_rate=60
log_file="battery_data.csv"
enable_log_file=false
enable_adjustment=true
# Single-instance PID and lock file locations
PID_FILE="/tmp/maintain_optimal_voltage.pid"
LOCK_FILE="/tmp/maintain_optimal_voltage.lock"
# Function to remove PID and lock files on exit or termination
cleanup() {
echo "Cleaning up..."
if [ -f "$PID_FILE" ] && [ "$(cat "$PID_FILE")" = "$$" ]; then
rm -f "$PID_FILE" "$LOCK_FILE"
fi
exit
}
# Trap to ensure cleanup on script termination
trap cleanup EXIT HUP INT QUIT TERM TSTP
# Ensure only one instance of the script runs
if [ -e "$LOCK_FILE" ]; then
if [ -e "$PID_FILE" ]; then
old_pid=$(cat "$PID_FILE")
if ps -p "$old_pid" > /dev/null 2>&1; then
echo "Another instance of the script is already running."
exit 1
else
echo "Removing stale PID file."
rm -f "$PID_FILE"
fi
fi
else
# Create a lock file
touch "$LOCK_FILE"
fi
# Create a PID file
echo $$ > "$PID_FILE"
# Function to display usage information
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -r, --rate SECONDS Update rate in seconds (default: 60)"
echo " -f, --file FILE CSV file for logging (default: battery_data.csv)"
echo " -l, --log-file Enable logging (default: disabled)"
echo " -a, --no-adjust Disable BCLM adjustment (default: enabled)"
echo " -v, --target-voltage V Target voltage (default: 3.92)"
echo " -d, --deadband V Deadband voltage (default: 0.02)"
echo " -h, --help Display this help message"
exit 1
}
# Function to validate voltage value
validate_voltage() {
local value=$1
local param_name=$2
if [[ ! $value =~ ^[0-9]+(\.[0-9]+)?$ ]] || (( $(echo "$value < 2.5" | bc -l) )) || (( $(echo "$value > 4.2" | bc -l) )); then
echo "Error: Invalid $param_name: $value. Must be between 2.5 and 4.2"
exit 1
fi
}
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-r|--rate)
if [[ $2 =~ ^[0-9]+$ ]] && (( $2 >= 5 && $2 <= 600 )); then
update_rate=$2
shift 2
else
echo "Invalid update rate: $2. Please specify a number between 5 and 600."
exit 1
fi
;;
-f|--file)
if [[ ! -d "$(dirname "$2")" ]]; then
echo "Error: Directory for CSV file does not exist: $(dirname "$2")"
exit 1
fi
log_file=$2
shift 2
;;
-v|--target-voltage)
validate_voltage "$2" "target voltage"
TARGET_VOLTAGE=$2
shift 2
;;
-d|--deadband)
if [[ ! $2 =~ ^[0-9]+(\.[0-9]+)?$ ]] || (( $(echo "$2 <= 0" | bc -l) )) || (( $(echo "$2 >= 0.5" | bc -l) )); then
echo "Error: Invalid deadband: $2. Must be between 0 and 0.5"
exit 1
fi
DEADBAND=$2
shift 2
;;
-l|--log-file)
enable_log_file=true
shift
;;
-a|--no-adjust)
enable_adjustment=false
shift
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1"
usage
;;
esac
done
# Check for sudo if adjustment is enabled
if $enable_adjustment && [[ $EUID -ne 0 ]]; then
echo "BCLM adjustment requires root privileges. Please run with sudo."
exit 1
fi
# Initialize CSV file with headers if not present
CSV_HEADER="Timestamp,Battery_Voltage,Voltage_MA,Battery_Percentage,Battery_Status,BCLM_Value,Battery_Rail_Current,Battery_Rail_Power,DC_In_Current,DC_In_Power,DC_In_Voltage,Battery_Temp"
initialize_csv() {
local log_file="$1"
if [ ! -f "$log_file" ]; then
if [ "$EUID" -eq 0 ]; then
sudo -u "$SUDO_USER" bash -c "echo '$CSV_HEADER' > '$log_file'"
else
echo "$CSV_HEADER" > "$log_file"
fi
echo "Initialized CSV file with header: $log_file"
fi
}
initialize_csv "$log_file"
# Calculate thresholds
LOWER_THRESHOLD=$(echo "$TARGET_VOLTAGE - $DEADBAND" | bc -l)
UPPER_THRESHOLD=$(echo "$TARGET_VOLTAGE + $DEADBAND" | bc -l)
MAX_ALLOWABLE_BATT_CHARGING_TEMP=37
echo "Target voltage range: $LOWER_THRESHOLD - $UPPER_THRESHOLD"
echo "Update rate: $update_rate seconds"
echo "CSV file: $log_file"
echo "Logging enabled: $enable_log_file"
echo "Adjustment enabled: $enable_adjustment"
# Initialize an array to store voltage readings
voltages=()
# Todo: test these different smoothing/filtering functions
# Define window sizes or periods for different filters
MEDIAN_AVERAGE_PERIOD=5 # Window size for median filter
EMA_ALPHA=0.2 # Smoothing factor for Exponential Moving Average
HAMPER_WINDOW_SIZE=3 # Window size for Hampel filter
GWMA_WINDOW_SIZE=7 # Window size for Gaussian Weighted Moving Average
GWMA_SIGMA=1.0 # Standard deviation for GWMA: Standard deviation, Controls the spread of the Gaussian function. Smaller values give more weight to closer points.
# Function to calculate the Median Filter
calculate_median_filter() {
sorted=($(printf '%s\n' "${voltages[@]}" | sort -n))
count=${#sorted[@]}
mid=$((count / 2))
if [ $((count % 2)) -eq 0 ]; then
# If count is even, calculate the average of the two middle values
median=$(echo "(${sorted[$mid-1]} + ${sorted[$mid]}) / 2" | bc -l)
else
# If count is odd, take the middle value
median=${sorted[$mid]}
fi
printf "%.5f\n" "$median"
}
# Function to calculate the Exponential Moving Average
calculate_ema() {
ema=${voltages[0]} # Start with the first value
for ((i=1; i<${#voltages[@]}; i++)); do
ema=$(echo "$EMA_ALPHA * ${voltages[$i]} + (1 - $EMA_ALPHA) * $ema" | bc -l)
done
printf "%.5f\n" "$ema"
}
# Same EMA but using a threshold value for spikes
calculate_ema_with_threshold() {
threshold=2.0 # Threshold for spike detection #todo: find this value
ema=${voltages[0]} # Start with the first value
for ((i=1; i<${#voltages[@]}; i++)); do
deviation=$(echo "${voltages[$i]} - $ema" | bc -l)
abs_deviation=$(echo "${deviation#-}" | bc -l)
if (( $(echo "$abs_deviation > $threshold" | bc -l) )); then
continue # Ignore this value, it's a spike
fi
ema=$(echo "$EMA_ALPHA * ${voltages[$i]} + (1 - $EMA_ALPHA) * $ema" | bc -l)
done
printf "%.5f\n" "$ema"
}
# Function to calculate the Hampel Filter
calculate_hampel_filter() {
window_size=$HAMPER_WINDOW_SIZE
threshold=2.0 # Example threshold for spike detection #todo: find -- also can use same value from spike-ema
filtered=()
for ((i=0; i<${#voltages[@]}; i++)); do
start=$((i-window_size))
end=$((i+window_size))
# Ensure indices are within bounds
[ $start -lt 0 ] && start=0
[ $end -ge ${#voltages[@]} ] && end=$((${#voltages[@]} - 1))
# Extract window and calculate median
window=("${voltages[@]:$start:$((end-start+1))}")
sorted_window=($(printf '%s\n' "${window[@]}" | sort -n))
mid=$(( ${#sorted_window[@]} / 2 ))
median=${sorted_window[$mid]}
# Check if the current value is a spike
deviation=$(echo "${voltages[$i]} - $median" | bc -l)
abs_deviation=$(echo "${deviation#-}" | bc -l)
if (( $(echo "$abs_deviation > $threshold" | bc -l) )); then
filtered+=("$median")
else
filtered+=("${voltages[$i]}")
fi
done
printf "%.5f\n" "${filtered[@]}"
}
# Function to calculate Gaussian Weighted Moving Average
calculate_gwma() {
local window_size=$GWMA_WINDOW_SIZE
local sigma=$GWMA_SIGMA
# If the number of voltages is less than the window size, adjust the window size
local num_voltages=${#voltages[@]}
if (( num_voltages < window_size )); then
window_size=$num_voltages
fi
# Precomputed Gaussian weights based on the distance from the center of the window.
weights=()
total_weight=0
for ((i=0; i<window_size; i++)); do
dist=$((i - (window_size / 2)))
# Use correct math for Gaussian weighting with bc
weight=$(echo "scale=5; e(-($dist * $dist) / (2 * $sigma * $sigma))" | bc -l)
weights+=("$weight")
total_weight=$(echo "$total_weight + $weight" | bc -l)
done
# Normalize weights
for ((i=0; i<window_size; i++)); do
weights[$i]=$(echo "${weights[$i]} / $total_weight" | bc -l)
done
# Calculate GWMA for the last available data point(s)
gwma=0
for ((j=0; j<window_size; j++)); do
weight=${weights[$j]}
value=${voltages[$(( (${#voltages[@]} - 1) - j ))]} # Ensure we use the right number of elements
gwma=$(echo "$gwma + $weight * $value" | bc -l)
done
# Return the single GWMA value
echo "$gwma"
}
# Function to get the current timestamp
get_timestamp() {
date +"%Y-%m-%d %H:%M:%S"
}
# Header row formatting
# Timestamp Batt% Voltage VoltageMA BCLM Status BattCurrent BattPower DCCurrent DCPower DCVoltage BattTemp
format="%-18s | %-3s | %-6s | %-6s | %-3s | %-16s | %-10s | %-10s | %-10s | %-10s | %-10s | %-9s\n"
# Print the header once
echo "--------------------------------------"
printf "$format" "Timestamp" "Batt%" "Voltage" "VoltMA" "BCLM" "Status" "Batt Current" "Batt Power" "DC Current" "DC Power" "DC Voltage" "Batt Temp"
### temporary thing:
# Determine the largest window size required among all filters
MAX_WINDOW_SIZE=$((MEDIAN_AVERAGE_PERIOD > GWMA_WINDOW_SIZE ? MEDIAN_AVERAGE_PERIOD : GWMA_WINDOW_SIZE))
MAX_WINDOW_SIZE=$((HAMPER_WINDOW_SIZE > MAX_WINDOW_SIZE ? HAMPER_WINDOW_SIZE : MAX_WINDOW_SIZE))
###
# todo: the window size for any of these should really depend on time, rather than just number of periods
# Main loop
while true; do
timestamp=$(get_timestamp)
current_voltage=$(read_voltage)
if [ $? -ne 0 ]; then
echo "Error: Unable to fetch voltage. Exiting." >&2
exit 1
fi
battery_status=$(check_battery_status)
battery_status_rc=$?
current_percentage=$(get_battery_percentage)
if [ $? -ne 0 ]; then
echo "Error: Unable to fetch battery percentage. Exiting." >&2
exit 1
fi
current_bclm=$(read_bclm)
new_bclm=$current_bclm # Initialize new_bclm with the current BCLM
# Get iSMC data
ismc_data=$(get_ismc_data)
# Parse relevant data from iSMC
battery_rail_current=$(echo "$ismc_data" | jq -r '.Current["Battery Rail"].quantity // 0')
battery_rail_power=$(echo "$ismc_data" | jq -r '.Power["Battery Rail"].quantity // 0')
dc_in_current=$(echo "$ismc_data" | jq -r '.Current["Mainboard S0 Rail (DC In)"].quantity // 0')
dc_in_power=$(echo "$ismc_data" | jq -r '.Power["Mainboard S0 Rail"].quantity // 0')
dc_in_voltage=$(echo "$ismc_data" | jq -r '.Voltage["DC In"].quantity // 0')
battery_temp=$(get_max_battery_temp "$ismc_data")
# error handling for battery temperature readings
if ! [[ "$battery_temp" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
echo "Error: Invalid temperature reading" >&2
battery_temp=0
fi
# Update the voltage readings array
voltages+=("$current_voltage")
if [ ${#voltages[@]} -gt $MAX_WINDOW_SIZE ]; then
voltages=("${voltages[@]:1}")
fi
# Calculate a filtered value from the voltage readings
filtered_voltage=$(calculate_ema)
# Output the current state before adjustments
printf "$format" "$timestamp" "$current_percentage" "$current_voltage" \
"$filtered_voltage" "$current_bclm" "$battery_status" \
"${battery_rail_current}A" "${battery_rail_power}W" \
"${dc_in_current}A" "${dc_in_power}W" \
"${dc_in_voltage}V" "${battery_temp}°C"
# Log the current state before adjustments if logging is enabled
if $enable_log_file; then
echo "$timestamp,$current_voltage,$filtered_voltage,$current_percentage,$battery_status_rc,$new_bclm,$battery_rail_current,$battery_rail_power,$dc_in_current,$dc_in_power,$dc_in_voltage,$battery_temp" >> "$log_file"
fi
# Adjust BCLM if enabled and within thresholds
if $enable_adjustment; then
if (( $(echo "$filtered_voltage >= $LOWER_THRESHOLD && $filtered_voltage <= $UPPER_THRESHOLD" | bc -l) )); then
# If Voltage is within Deadband
new_bclm=$current_percentage
elif (( $(echo "$filtered_voltage < $LOWER_THRESHOLD" | bc -l) )); then
# If Voltage is Below Deadband:
if [[ $battery_status == "AC not charging" || $battery_status == "AC charged" ]] && ((current_bclm < MAX_BCLM)); then
new_bclm=$((current_percentage + 1))
fi
# Adjust based on battery temperature
if [[ $battery_status == "AC charging" ]] && (( $(echo "$battery_temp > $MAX_ALLOWABLE_BATT_CHARGING_TEMP" | bc -l) )); then
new_bclm=$((current_percentage - 1)) # Reduce charging if battery is warm
echo "Battery too hot. Not charging."
fi
elif (( $(echo "$filtered_voltage > $UPPER_THRESHOLD" | bc -l) )); then
# If Voltage is Above Deadband: #TODO - i think there are more cases to handle in here.
if ((current_bclm > current_percentage - 2)); then
new_bclm=$((current_percentage - 2))
fi
fi
# Clamp BCLM within limits
new_bclm=$((new_bclm > MAX_BCLM ? MAX_BCLM : new_bclm))
new_bclm=$((new_bclm < MIN_BCLM ? MIN_BCLM : new_bclm))
# Apply the new BCLM if it has changed
if (( new_bclm != current_bclm )); then
write_bclm $new_bclm
echo "Adjusted BCLM to: $new_bclm"
fi
fi
sleep "$update_rate"
done