-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpower_test.py
executable file
·328 lines (279 loc) · 11.7 KB
/
power_test.py
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
#!/usr/bin/env python3
"""
Power board test
To run this with an SRv4 Power Board connect power resistors to all the 12V
outputs and the 5V output. Size the resistors so that the total current with
all 12V outputs enabled is between 10A and 25A.
The test will:
- Enable and disable the run and error LEDs
- Play the buzzer
- Detect the start button being pressed
- Test the current draw on each output with and without the output enabled
- Test the global current with multiple outputs enabled
The brain output is set by the BRAIN_OUTPUT constant below.
"""
import argparse
import atexit
import csv
import logging
import os
import textwrap
import threading
from time import sleep
import serial
import sbot.power_board
from sbot.exceptions import BoardDisconnectionError
from sbot.logging import setup_logging
from sbot.power_board import PowerBoard, PowerOutputPosition
from sbot.utils import singular
BRAIN_OUTPUT = PowerOutputPosition.L2
OUTPUT_RESISTANCE = [
1.5, # H0
1.5, # H1
1.5, # L0
6.0, # L1
6.0, # L2
1.5, # L3
10.0, # 5V
]
setup_logging(False, False)
logger = logging.getLogger("tester")
# monkey patch which output is the brain output
sbot.power_board.BRAIN_OUTPUT = BRAIN_OUTPUT
run_led_thread = True
def log_and_assert_bounds(results, key, value, name, unit, min, max):
logger.info(f"Detected {name}: {value:.3f}{unit}")
results[key] = value
center = (min + max) / 2
variance = (max - min) / 2
assert min < value < max, (
f"{name.capitalize()} of {value:.3f}{unit} is outside acceptable range of "
f"{center:.2f}±{variance:.2f}{unit}.")
def log_and_assert(results, key, value, name, unit, nominal, tolerance):
logger.info(f"Detected {name}: {value:.3f}{unit}")
results[key] = value
min = nominal * (1 - tolerance)
max = nominal * (1 + tolerance)
assert min < value < max, (
f"{name.capitalize()} of {value:.3f}{unit} is outside acceptable range of "
f"{nominal:.2f}±{tolerance:.0%}.")
def test_output(board, results, output, input_voltage):
if output == PowerOutputPosition.FIVE_VOLT:
test_regulator(board, results)
return
log_and_assert_bounds( # test off current
results, f'out_{output.name}_off_current', board.outputs[output].current,
f'output {output.name} off state current', 'A', -0.2, 0.2)
# enable output
if BRAIN_OUTPUT == output:
board._serial.write('*SYS:BRAIN:SET:1')
else:
board.outputs[output].is_enabled = True
sleep(0.5)
expected_out_current = input_voltage / OUTPUT_RESISTANCE[output]
logger.debug(f"Expected current: {expected_out_current}, ({input_voltage} / {OUTPUT_RESISTANCE[output]})")
log_and_assert( # test output current
results, f'out_{output.name}_current', board.outputs[output].current,
f'output {output.name} current', 'A', expected_out_current, 0.2)
log_and_assert( # test global current
results, f'out_{output.name}_global_current', board.battery_sensor.current,
'global output current', 'A', expected_out_current, 0.2)
# disable output
if BRAIN_OUTPUT == output:
board._serial.write('*SYS:BRAIN:SET:0')
else:
board.outputs[output].is_enabled = False
sleep(0.5)
def test_regulator(board, results):
# test off current
log_and_assert_bounds(
results, 'reg_off_current', board.outputs[PowerOutputPosition.FIVE_VOLT].current,
'regulator off state current', 'A', -0.2, 0.2)
# enable output
if BRAIN_OUTPUT == PowerOutputPosition.FIVE_VOLT:
board._serial.write('*SYS:BRAIN:SET:1')
else:
board.outputs[PowerOutputPosition.FIVE_VOLT].is_enabled = True
sleep(0.5)
reg_voltage = board.status.regulator_voltage
log_and_assert_bounds(
results, 'reg_volt', reg_voltage, 'regulator voltage', 'V', 4.5, 5.5)
expected_reg_current = reg_voltage / OUTPUT_RESISTANCE[PowerOutputPosition.FIVE_VOLT]
log_and_assert(
results, 'reg_current', board.outputs[PowerOutputPosition.FIVE_VOLT].current,
'regulator current', 'A', expected_reg_current, 0.2)
# disable output
if BRAIN_OUTPUT == PowerOutputPosition.FIVE_VOLT:
board._serial.write('*SYS:BRAIN:SET:0')
else:
board.outputs[PowerOutputPosition.FIVE_VOLT].is_enabled = False
sleep(0.5)
def led_flash(board):
"""Flash the run and error LEDs out of phase"""
while run_led_thread:
board._run_led.on()
board._error_led.off()
sleep(0.5)
board._run_led.off()
board._error_led.on()
sleep(0.5)
def test_board(output_writer, test_uvlo):
global run_led_thread
results = {}
board = singular(PowerBoard._get_supported_boards())
try:
results['passed'] = False # default to failure
# Unregister the cleanup as we have our own
atexit.unregister(board._cleanup)
board_identity = board.identify()
results['asset'] = board_identity.asset_tag
results['sw_version'] = board_identity.sw_version
logger.info(
f"Running power board test on board: {board_identity.asset_tag} "
f"running firmware version: {board_identity.sw_version}.")
board.reset()
# disable brain output
board._serial.write('*SYS:BRAIN:SET:0')
sleep(0.5)
# expected currents are calculated using this voltage
input_voltage = board.battery_sensor.voltage
log_and_assert_bounds(
results, 'input_volt', input_voltage, 'input voltage', 'V', 11.5, 12.5)
# fan
# force the fan to run
board._serial.write('*SYS:FAN:SET:1')
fan_result = input("Is the fan running? [Y/n]") or 'y' # default to yes
results['fan'] = fan_result
assert fan_result.lower() == 'y', "Reported that the fan didn't work."
board._serial.write('*SYS:FAN:SET:0')
# buzzer
board.piezo.buzz(1000, 0.5)
buzz_result = input("Did the buzzer buzz? [Y/n]") or 'y' # default to yes
results['buzzer'] = buzz_result
assert buzz_result.lower() == 'y', "Reported that the buzzer didn't buzz."
# leds
run_led_thread = True
flash_thread = threading.Thread(target=led_flash, args=(board,), daemon=True)
flash_thread.start()
led_result = input("Are the LEDs flashing? [Y/n]") or 'y' # default to yes
results['leds'] = led_result
assert led_result.lower() == 'y', "Reported that the LEDs didn't work."
run_led_thread = False
flash_thread.join()
board._run_led.off()
board._error_led.off()
# start button
board._start_button()
logger.info("Please press the start button")
while not board._start_button():
sleep(0.1)
results['start_btn'] = "y"
for output in PowerOutputPosition:
test_output(board, results, output, input_voltage)
total_expected_current = 0
for output in PowerOutputPosition:
if output == BRAIN_OUTPUT:
continue
total_expected_current += input_voltage / OUTPUT_RESISTANCE[output]
if total_expected_current > 25.0:
# stop before we hit the current limit
break
board.outputs[output].is_enabled = True
sleep(0.5)
log_and_assert(
results, f'sum_out_{output.name}_current', board.battery_sensor.current,
f'output current up to {output.name}', 'A', total_expected_current, 0.1)
sleep(0.5)
# disable all outputs
board.outputs.power_off()
if test_uvlo:
try:
psu = serial.serial_for_url('hwgrep://0416:5011')
except serial.SerialException:
logger.error("Failed to connect to PSU")
return
psu.write(b'VSET1:11.5\n')
# Enable output
psu.write(b'OUT1\n')
# start at 11.5V and drop to 10V
for voltx10 in range(115, 100, -1):
psu.write(f'VSET1:{voltx10 / 10}\n'.encode('ascii'))
sleep(0.1)
# stop when serial is lost
try:
meas_voltage = board.battery_sensor.voltage
logger.info(f"Measured voltage: {meas_voltage}V for {voltx10 / 10}V")
except BoardDisconnectionError:
logger.info(f"Software UVLO triggered at {voltx10 / 10}V")
results['soft_uvlo'] = voltx10 / 10
break
else:
assert False, "Software UVLO didn't function at 10V."
# set to 9.5V and ask if leds are off
psu.write(b'VSET1:9.5\n')
sleep(0.1)
# default to yes
hard_uvlo_result = input("Have all the LEDs turned off? [Y/n]") or 'y'
results['hard_uvlo'] = hard_uvlo_result
assert hard_uvlo_result.lower() == 'y', \
"Reported that hardware UVLO didn't function."
# set to 10.9V-11.3V and check if serial is back
for voltx10 in range(109, 114):
psu.write(f'VSET1:{voltx10 / 10}\n'.encode('ascii'))
sleep(2)
# stop when serial is back
try:
meas_voltage = board.battery_sensor.voltage
logger.info(f"Measured voltage: {meas_voltage}V for {voltx10 / 10}V")
except BoardDisconnectionError:
pass
else:
logger.info(f"Hardware UVLO cleared at {voltx10 / 10}V")
results['hard_uvlo_hyst'] = voltx10 / 10
break
else:
assert False, "Hardware UVLO didn't clear at 11.3V."
# Disable output
psu.write(b'OUT0\n')
logger.info("Board passed")
results['passed'] = True
finally:
if output_writer is not None:
output_writer.writerow(results)
# Disable all outputs
board.reset()
board._serial.write('*SYS:BRAIN:SET:0')
def main():
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=textwrap.dedent(__doc__))
parser.add_argument('--log', default=None, help='A CSV file to save test results to.')
parser.add_argument('--test-uvlo', action='store_true', help='Test the UVLO circuit.')
args = parser.parse_args()
if args.log:
new_log = True
if os.path.exists(args.log):
new_log = False
with open(args.log, 'a', newline='') as csvfile:
fieldnames = [
'asset', 'sw_version', 'passed', 'input_volt',
'reg_volt', 'reg_current', 'reg_off_current',
'out_H0_off_current', 'out_H0_current', 'out_H0_global_current',
'out_H1_off_current', 'out_H1_current', 'out_H1_global_current',
'out_L0_off_current', 'out_L0_current', 'out_L0_global_current',
'out_L1_off_current', 'out_L1_current', 'out_L1_global_current',
'out_L2_off_current', 'out_L2_current', 'out_L2_global_current',
'out_L3_off_current', 'out_L3_current', 'out_L3_global_current',
'sum_out_H0_current', 'sum_out_H1_current', 'sum_out_L0_current',
'sum_out_L1_current', 'sum_out_L2_current', 'sum_out_L3_current',
'sum_out_FIVE_VOLT_current',
'fan', 'leds', 'buzzer', 'start_btn',
'soft_uvlo', 'hard_uvlo', 'hard_uvlo_hyst']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
if new_log:
writer.writeheader()
test_board(writer, args.test_uvlo)
else:
test_board(None, args.test_uvlo)
if __name__ == '__main__':
main()