class Camera(Instrument): # ? https://pypi.org/project/simple-pyspin/ _config = configuration.from_yml( r'W:\Test Data Backup\instruments\config\camera.yml') display_name = _config.field(str) EXPOSURE_TIME_US = _config.field(float) TX_WAIT_S = 0. def _instrument_cleanup(self) -> None: self.interface.close() def _instrument_setup(self) -> None: # TODO: gain settings? AOI settings? neither are used in labview version self.interface = Spinnaker() self.interface.init() self.interface.PixelFormat = 'RGB8' self.interface.ExposureAuto = 'Off' self.interface.ExposureMode = 'Timed' self.interface.ExposureTime = self.EXPOSURE_TIME_US @proxy.exposed def capture(self): """ takes 212ms including start and stop this camera model is specced as 7.5FPS """ self.interface.start() img = self.interface.get_array() self.interface.stop() return img def _instrument_check(self) -> None: _ = self.interface.initialized def _instrument_debug(self) -> None: from cv2 import cv2 cv2.imshow('debug image', self.capture()) cv2.waitKey(0)
class Pixie2pt0Station(TestStation): bk = TestInstrument(BKPowerSupply(), logging.INFO) lm = TestInstrument(LightMeter(), logging.INFO) ftdi = TestInstrument(RS485(), logging.DEBUG) nfc = TestInstrument(NFC(), logging.INFO) _config = configuration.from_yml('pixie_hack/.yml') TESTING_FW_PATH = _config.field(str) PRODUCTION_FW_PATH = _config.field(str) RESULT_PATH = _config.field(str) PARAMS_PATH = _config.field(str) ASSOCIATION_TABLE_PATH = _config.field(str) POWER_ON_WAIT_S = _config.field(float) CH_SETTLE_WAIT_S = _config.field(float) AFTER_ERASE_WAIT_S = _config.field(float) PROGRAMMING_V = _config.field(float) PROGRAMMING_I = _config.field(float) def set_power_supply_for_programming(self) -> None: self.bk.write_settings(DCLevel(self.PROGRAMMING_V, self.PROGRAMMING_I)) def get_params(self) -> None: sheet = pd.read_excel(self.PARAMS_PATH, comment='#', sheet_name='params') rows = cast(List[Dict], sheet.to_dict('records')) rows.sort(key=itemgetter('row')) self.params = [messages.Param(**row) for row in rows] list(map(self.put, self.params)) def __init__(self, to_view: ThreadConnection = None) -> None: self.q = to_view self.put = to_view.put if to_view is not None else self.info self.instrument_setup() self.set_power_supply_for_programming() self.bk.write_settings(output_state=False) self.masks = [1 << ch for ch in range(4)] + [15] self.string_results = list() self.params: List[messages.Param] = list() self.uid_table = dict() self.dut_id_table = dict() def pixie_ch_command(self, mask: int) -> None: self.ftdi.ser.send_ascii(f'p{mask}\n') def programming_message_adapter(self, msg) -> None: self.put(getattr(messages, type(msg).__name__)(**asdict(msg))) def program(self, fp: str) -> None: self.set_power_supply_for_programming() sleep(.1) self.ftdi.ser.send_ascii('U') sleep(self.AFTER_ERASE_WAIT_S) self.ftdi.dta_program_firmware(fp, self.programming_message_adapter) def string_check(self, param: messages.Param) -> None: self.bk.write_settings(DCLevel(param.v, param.i)) [self.pixie_ch_command(param.ch_mask) for _ in range(20)] sleep(self.CH_SETTLE_WAIT_S) light = self.lm.measure() power = self.bk.measure() dist = light.distance_from(param) dist_pf = dist <= param.color_dist_max fcd_pf = (param.fcd_nom - param.fcd_tol) <= light.fcd <= ( param.fcd_nom + param.fcd_tol) p_pf = (param.p_nom - param.p_tol) <= power.P <= (param.p_nom + param.p_tol) self.pixie_ch_command(0) result = messages.Result(param.row, light.x, light.y, dist, light.fcd, power.P, dist_pf, fcd_pf, p_pf) self.put(result) self.string_results.append(result) sleep(.1) def test(self, dut: messages.DUT) -> None: self.put(dut) self.bk.write_settings(output_state=True) sleep(self.AFTER_ERASE_WAIT_S) self.program(self.TESTING_FW_PATH) winsound.Beep(1500, 250) sleep(self.AFTER_ERASE_WAIT_S) self.put(messages.StringsStart()) self.string_results.clear() [self.string_check(param) for param in self.params] test_pf = all(result.row_pf for result in self.string_results) with open(self.RESULT_PATH, 'a', newline='') as wf: writer = csv.DictWriter(wf, result_header) writer.writerows([ dict(dut_id=dut.dut_id, t=datetime.now(), test_pf=test_pf, **asdict(result)) for result in self.string_results ]) self.program(self.PRODUCTION_FW_PATH) self.bk.write_settings(output_state=False) for _ in range(2): winsound.Beep(1500, 250) sleep(.250) winsound.Beep(2500 if test_pf else 1000, 1000) self.dut_id_table[dut.dut_id] = dict(test_pf=test_pf, **asdict(dut)) self.write_association_table() self.put(messages.TestResult(test_pf)) # noinspection PyMethodMayBeStatic def nfc_present(self) -> bool: # TODO: return self.nfc.is_present() # noinspection PyMethodMayBeStatic def get_uid(self) -> str: # TODO: [self.nfc.interface.reset_input_buffer() for _ in range(5)] first = self.nfc.read_uid() if self.nfc.read_uid() == first and len(first) > 5: return first return '' def read_association_table(self) -> None: self.uid_table.clear() self.dut_id_table.clear() with open(self.ASSOCIATION_TABLE_PATH, newline='') as rf: for row in csv.DictReader(rf): row['dut_id'] = int(row['dut_id']) self.dut_id_table[row['dut_id']] = row self.uid_table[row['uid']] = row def write_association_table(self) -> None: with open(self.ASSOCIATION_TABLE_PATH, 'w+', newline='') as wf: writer = csv.DictWriter(wf, assoc_table_header) writer.writeheader() writer.writerows(list(self.dut_id_table.values())) # noinspection PyMethodMayBeStatic def get_dut_id(self, uid: str) -> int: # TODO: self.read_association_table() if not self.uid_table: return 1 row = self.uid_table.get(uid, None) if row is None: return max(self.dut_id_table.keys()) + 1 return row['dut_id'] def should_stop(self) -> bool: # TODO: try: self.q.get_nowait() except queue.Empty: pass except (SentinelReceived, ConnectionClosed): return True def mainloop(self) -> None: try: self.get_params() while 1: if self.nfc_present(): uid = self.get_uid() if uid: winsound.Beep(1000, 500) self.read_association_table() self.test(messages.DUT(self.get_dut_id(uid), uid)) if self.should_stop(): return self.q.put_sentinel() except Exception: self.q.put_sentinel() raise
class NFC(Serial): _config = configuration.from_yml(r'instruments\nfc_probe.yml') display_name = _config.field(str) TIMEOUT = _config.field(float) TX_WAIT_S = _config.field(float) COMMAND_RETRIES = _config.field(int) ACTION_RETRIES = _config.field(int) READ_BLOCK_ATTEMPTS = _config.field(int) READ_BLOCK_SUCCESSIVE = _config.field(int) REDUNDANT_ATTEMPTS = _config.field(int) WRITE_BLOCKS = _config.field(tuple) SN_BLOCKS = _config.field(dict) MN_BLOCKS = _config.field(dict) # DEVICE_NAME = "Arduino LilyPad USB" HWID = r'5&33EB7B70&0&7' BAUDRATE = 57600 XON_X_OFF = False ENCODING = 'utf-8' TERM_CHAR = '\n' class __Commands: is_present = "X" read_uid = "U" set_block = "B%02x" write_block = "W%08x" read_block = "R" BLOCKS: List[List[int]] @register.before('__init__') def _make_nfc_attrs(self) -> None: self.BLOCKS = [[v for k, v in d.items() if k in self.WRITE_BLOCKS] for d in (self.SN_BLOCKS, self.MN_BLOCKS)] if not all(self.BLOCKS): raise AttributeError('must specify at least one of [new, old] in write_blocks') self._read_tail = deque(maxlen=self.READ_BLOCK_SUCCESSIVE) def _command(self, command: str, arg: Optional[int]) -> str: if '%' in command: if arg is None: raise NFCError(f'{command} takes no argument') command = command % arg for i in range(self.COMMAND_RETRIES): self.proxy_check_cancelled() self.write(command) rx = self.read() if rx: return rx raise NFCError(f'no response to command "{command}"') def _action(self, command_string, arg: Optional[int] = None): for i in range(self.ACTION_RETRIES): # noinspection PyBroadException try: return self._command(command_string, arg) except NFCError: self.instrument_setup() continue raise NFCError(f'failed: {command_string}({arg})') def _set_block(self, index: int) -> None: if not index == int(self._action(self.__Commands.set_block, index)[:-1], 16): raise NFCError(f'failed to set block to {index}') def _write_block(self, payload: int) -> None: if not 'Y' == self._action(self.__Commands.write_block, payload): raise NFCError(f'failed to write block to {payload}') def _read_block(self) -> int: for _ in range(self.REDUNDANT_ATTEMPTS): self._read_tail.clear() for _ in range(self.READ_BLOCK_ATTEMPTS): value = int(self._action(self.__Commands.read_block)[:-1], 16) self._read_tail.append(value) if self._read_tail.count(value) == self.READ_BLOCK_SUCCESSIVE: return value raise NFCError('inconsistent _read_block iterations') @proxy.exposed def read_register(self, index: int): self._set_block(index) return self._read_block() @proxy.exposed def write_register(self, index: int, payload: int): self._set_block(index) self._write_block(payload) if self._read_block() != payload: raise NFCError(f'failed to confirm write_register({index}, {payload})') @proxy.exposed def is_present(self): return 'Y' == self._action(self.__Commands.is_present) @proxy.exposed def read_uid(self): return self._action(self.__Commands.read_uid) @proxy.exposed def write_unit_identity(self, sn: int, mn: int): for blocks, payload in zip(self.BLOCKS, (sn, mn)): blocks: List[int] [self.write_register(block, payload) for block in blocks] @proxy.exposed def get_registers(self): response = {} for blocks in self.BLOCKS: for index in blocks: response[index] = self.read_register(index) return response def _instrument_check(self) -> None: self._action(self.__Commands.is_present) @proxy.exposed def test(self) -> None: [self.info(f'is_present = {self.is_present()}') for _ in range(25)] def _instrument_debug(self) -> None: self.test()
class LambdaPowerSupply(Serial, _DCPowerSupply): def calculate_knee(self, percent_of_max: float) -> DCLevel: raise NotImplementedError _config = configuration.from_yml(r'config\lambda_power_supply.yml') display_name = _config.field(str) PORT = _config.field(str) BAUDRATE = _config.field(int) TIMEOUT = _config.field(float) TX_WAIT_S = _config.field(float) SET_SUCCESS_MARGIN = _config.field(float) COMMAND_RETRIES = _config.field(int) XON_X_OFF = False ENCODING = 'utf-8' TERM_CHAR = '' _set_address = Command(r'ADR01') _clear_error_registers = Command('DCL') _set_uvp_level = Command('UVP%04.1f') _set_ovp_level = Command('OVP%04.1f') _get_measurement = Command( 'STT?', 'AV{v_meas:f}SV{v_set:f}AA{i_meas:f}SA{i_set:f}OS{os}AL{al}PS{ps}') _set_output_state = Command('OUT%d') _set_voltage = Command('VOL%05.2f') _set_current = Command('CUR%06.3f') def __read_settings(self) -> Tuple[DCLevel, DCLevel]: """ returns settings obj, measurement obj """ results = cast(STTResponse, self._get_measurement()) self.alarms = AlarmStatusRegister(results.al) self.operations_status = OperationStatusRegister(results.os) self.error_codes = ErrorCodesRegister(results.ps) return DCLevel(results.v_meas, results.i_meas), DCLevel(results.v_set, results.i_set) @proxy.exposed def measure(self, fresh: bool = True) -> DCLevel: _ = fresh return self.__read_settings()[0] @proxy.exposed def set_settings(self, dc_level: DCLevel) -> None: self._set_voltage(dc_level.V) self._set_current(dc_level.A) @proxy.exposed def set_output(self, output_state: bool) -> None: self._set_output_state(output_state) def get_settings(self) -> DCLevel: raise NotImplementedError def get_output(self) -> bool: raise NotImplementedError @proxy.exposed def write_settings(self, dc_level: DCLevel = None, output_state: bool = None): if dc_level is None and output_state is None: raise LambdaPowerSupplyError( 'must call .write_settings() with at least one arg') if dc_level is not None: self.set_settings(dc_level) if output_state is not None: self.set_output(output_state) error_strings = [] _, settings = self.__read_settings() if dc_level is not None: _margin = self.SET_SUCCESS_MARGIN for read, exp in ((settings.V, dc_level.V), (settings.A, dc_level.A)): if not ((exp - _margin) < read < (exp + _margin)): error_strings.append(dc_level) break else: self.debug(f'power settings = {settings}') if output_state is not None: if self.operations_status.IS_OUTPUT ^ output_state: error_strings.append(f'output_enable={output_state}') else: self.debug(f'output state = {output_state}') if error_strings: raise LambdaPowerSupplyError(f'failed to set to ' + ', '.join(error_strings)) @register.after('_instrument_setup') def _lambda_setup(self) -> None: self._set_address() self._set_uvp_level(0.) self._set_ovp_level(60.) self._lambda_cleanup() if self.alarms or self.error_codes: self._clear_error_registers() @register.before('_instrument_cleanup') def _lambda_cleanup(self) -> None: self.write_settings(DCLevel(0., 0.), False) def _instrument_check(self) -> None: self._get_measurement() @proxy.exposed def test(self): for _ in range(10): self.write_settings(DCLevel(12, 0.1), True) self.write_settings(DCLevel(14, 0.2)) self.write_settings(DCLevel(16, 0.3)) self.write_settings(DCLevel(10, 0.4)) self.write_settings(output_state=False) def _instrument_debug(self) -> None: self.test()
class ChromaPowerSupply(Serial): # TODO check status register endianness # TODO check OPC fidelity # TODO add inrush delay from test settings command arg to AC measurements or error maybe # TODO measure time to execute exposed methods # TODO merge leak tester command group idiom to this # TODO documentation at least at module level # ? W:\TestStation Data Backup\instruments\data\UM-61601~4-acsource-v1.9-102015.pdf # ? pg. 67 -> remote operation # ? W:\TestStation Data Backup\instruments\data\QSG-61601~4-acsource-v1.0-022010.pdf # ? W:\TestStation Data Backup\instruments\data\UM-615,616XX-SoftPanel-v1.6-082013.pdf _config = configuration.from_yml(r'W:\Test Data Backup\instruments\config\chroma_power_supply.yml') DEVICE_NAME = _config.field(str) BAUDRATE = _config.field(int) TIMEOUT = _config.field(float) TX_WAIT_S = _config.field(float) COMMAND_EXEC_WAIT_S = _config.field(float) XON_X_OFF = False ENCODING = 'utf-8' TERM_CHAR = '\r\n' _command_config = CommandReader(r'W:\Test Data Backup\instruments\data\Command Source.csv') voltage_range = _command_config.setting(True) output_state = _command_config.setting() output_coupling = _command_config.setting() current_limit = _command_config.setting() ocp_delay_s = _command_config.setting() inrush_start_time_ms = _command_config.setting() inrush_measurement_interval_ms = _command_config.setting() output_frequency = _command_config.setting() _vac_high_range = _command_config.setting() _vdc_high_range = _command_config.setting() _vac_low_range = _command_config.setting() _vdc_low_range = _command_config.setting() vac_limit = _command_config.setting() vdc_plus_limit = _command_config.setting() vdc_minus_limit = _command_config.setting() phase_on = _command_config.setting() phase_off = _command_config.setting() ac_slew_rate = _command_config.setting() dc_slew_rate = _command_config.setting() freq_slew_rate = _command_config.setting() i_rms = _command_config.measurement() ipk = _command_config.measurement() cf = _command_config.measurement() inrush = _command_config.measurement() true_power = _command_config.measurement() apparent_power = _command_config.measurement() reactive_power = _command_config.measurement() pf = _command_config.measurement() v_rms = _command_config.measurement() vdc = _command_config.measurement() idc = _command_config.measurement() ntr_register = _command_config.status(NTRRegister) event_status_register = _command_config.status(EventStatusRegister) status_byte_register = _command_config.status(StatusByteRegister) clear_protection_latch = _command_config.command() clear_registers = _command_config.command() vdc_setting: Setting vac_setting: Setting @register.after('__init__') def _set_state_constants(self) -> None: self._voltage_range_callback('LOW') @register.after('_instrument_setup') def _set_output_constant_values(self) -> None: # noinspection PyCallingNonCallable self.output_coupling('ACDC') self.setting(ChromaGenSettings()) # noinspection PyTypeChecker def _voltage_range_callback(self, v_range: str) -> None: _range = _RangeState[v_range] self.vdc_setting = {_RangeState.HIGH: self._vdc_high_range, _RangeState.LOW: self._vdc_low_range}[_range] self.vac_setting = {_RangeState.HIGH: self._vac_high_range, _RangeState.LOW: self._vac_low_range}[_range] @proxy.exposed def setting(self, condition: Union[SettingMessage], delay_override: float = None) -> None: for command in condition.requests_in_series(self): self.write(command) self._instrument_delay(delay_override or self.COMMAND_EXEC_WAIT_S) condition.verify(self) # noinspection PyCallingNonCallable @proxy.exposed def output_enable(self) -> None: self.clear_protection_latch() self.output_state('ON') @proxy.exposed @register.before('_instrument_cleanup') @register.after('_instrument_setup') def output_disable(self) -> None: self.setting(OutputOffCondition()) @proxy.exposed def operation_completed_bit(self) -> bool: self.write('*OPC?') return bool(int(self.read())) @proxy.exposed def check_status_registers(self) -> 'StatusRegisters': return StatusRegisters.fulfill(self) def _instrument_check(self) -> None: ntr = self.check_status_registers().ntr_register if ntr: raise ChromaPowerSupplyError(ntr) def _instrument_debug(self) -> None: from time import perf_counter for vdc in range(5, 15): ti = perf_counter() self.write_settings(float(vdc), 0., 60., 0.5) self.info('settings', perf_counter() - ti) ti = perf_counter() self.output_enable() self.info('output enable', perf_counter() - ti) ti = perf_counter() self.info(self.measure()) self.info('measure', perf_counter() - ti) ti = perf_counter() self.output_disable() self.info('output disable', perf_counter() - ti) @proxy.exposed def write_settings(self, vdc: float, vac: float, freq: float, i_limit: float, phase_on: float = 0., phase_off: float = 0.) -> None: self.setting(_ChromaTestCondition(ChromaTestCondition(vdc, vac, freq, i_limit, phase_on, phase_off))) @proxy.exposed def measure(self) -> ChromaMeasurement: return ChromaMeasurement.fulfill(self)
class HipotTester(Serial): # ? W:\TestStation Data Backup\instruments\data\GPT-9800-m.pdf _config = configuration.from_yml( r'W:\Test Data Backup\instruments\config\hipot_tester.yml') display_name = _config.field(str) TEST_DURATION_MARGIN = _config.field(float) DELAY_BETWEEN_MEASUREMENTS_S = _config.field(float) HWID = _config.field(str) BAUDRATE = _config.field(int) TIMEOUT = _config.field(float) TX_WAIT_S = _config.field(float) ERROR_CHECK_ONLY_AFTER_LOAD = _config.field(bool) XON_X_OFF = False ENCODING = 'utf-8' TERM_CHAR = '\r\n' _get_identify = Command('*IDN?', '{}GPT{}') _get_errors = Command( 'SYST:ERR ?', '{errors}') # returns error strings from pg.136 of the docs _clear_errors = Command('*CLS') # clears internal registers _get_measurement = Command('MEAS ?', _test_measurement_proto) _get_test_settings = Command('MANU%d:EDIT:SHOW ?', _test_settings_proto) _start_test = Command('FUNC:TEST ON') _stop_test = Command('FUNC:TEST OFF') # for DC and AC _set_to_manual = Command('MAIN:FUNC MANU') _set_test_type = Command('MANU:EDIT:MODE %s') # {ACW, DCW} _set_ramp_t = Command('MANU:RTIM %f') # 0.1~999.9 seconds _set_test_number = Command('MANU:STEP %d') # 1-100 _set_arc_mode = Command('MANU:UTIL:ARCM %s') # {OFF, ON_CONT, ON_STOP} _set_ground_mode = Command('MANU:UTIL:GROUNDMODE %s') # {ON, OFF} _get_test_number = Command('MANU:STEP ?', '{test_program:d}') _get_arc_mode = Command('MANU:UTIL:ARCM ?', 'ARC {arc_mode}') _get_ground_mode = Command('MANU:UTIL:GROUNDMODE ?', '{gound_mode}') # for AC _set_ac_voltage = Command('MANU:ACW:VOLT %.3f') # 0.1-5.0kV _set_ac_upper = Command('MANU:ACW:CHIS %f') # 0.001 ~ 042.0mA _set_ac_lower = Command('MANU:ACW:CLOS %f') # 0.000 ~ 041.9mA _set_ac_test_t = Command('MANU:ACW:TTIM %f') # 0.5 ~ 999.9 seconds _set_ac_frequency = Command('MANU:ACW:FREQ %d') # {50, 60} Hz _set_ac_ref_current = Command('MANU:ACW:REF %f') # 0.000 ~ 041.9mA _set_ac_arc_current = Command( 'MANU:ACW:ARCC %f') # 0.000 ~ 080.0mA (<2x upper) _get_ac_frequency = Command('MANU:ACW:FREQ ?', '{frequency:d} Hz') _get_ac_ref_current = Command('MANU:ACW:REF ?', '{ref_current:f}mA') _get_ac_arc_current = Command('MANU:ACW:ARCC ?', '{arc_current:f}mA') # for DC _set_dc_voltage = Command('MANU:DCW:VOLT %f') # 0.100 ~ 6.100kV _set_dc_upper = Command('MANU:DCW:CHIS %f') # 0.001 ~ 11.00mA _set_dc_lower = Command('MANU:DCW:CLOS %f') # 0.000 ~ 010.9mA _set_dc_test_t = Command('MANU:DCW:TTIM %f') # 0.5 ~ 999.9 seconds _set_dc_ref_current = Command('MANU:DCW:REF %f') # 000.0 ~ 010.9mA _set_dc_arc_current = Command('MANU:DCW:ARCC %f') # 000.0 ~ 22.00mA _get_dc_ref_current = Command('MANU:DCW:REF ?', '{ref_current:f}mA') _get_dc_arc_current = Command('MANU:DCW:ARCC ?', '{arc_current:f}mA') _still_testing_steps = {'TEST', 'VIEW'} @register.after('_instrument_setup') def _hipot_setup(self) -> None: self._clear_errors() self._set_to_manual() def _instrument_check(self) -> None: self._get_identify() def check_for_error_code(self, packet: str) -> None: errors = self._get_errors() if errors and (errors != '0,No Error'): self._clear_errors() raise HipotTesterError( f'got error codes "{errors}" after {packet}') def set_test_program(self, test_program: int) -> None: self._set_test_number(test_program) if test_program != self._get_test_number(): raise HipotTesterError( f'failed to set test program to {test_program}') def __run_test(self, test_specs: HipotProgram, consumer: HIPOT_CONSUMER = None) -> bool: # type: ignore consumer = consumer if callable(consumer) else self.debug consumer(test_specs) # type: ignore self._start_test() max_test_len = test_specs.total_t + self.TEST_DURATION_MARGIN # type: ignore tf = max_test_len + time() # type: ignore last_t, do_delay, ramp_te = None, True, 0. while 1: self._instrument_delay( self.DELAY_BETWEEN_MEASUREMENTS_S if do_delay else 0.) do_delay = True try: meas = HipotMeasurement( **self._get_measurement()) # type: ignore except HipotTesterError as e: do_delay = False if time() > tf: raise HipotTesterError( f'failed to receive test result after {max_test_len}' ) from e else: if meas.time_elapsed != last_t: last_t = meas.time_elapsed if meas.step == 'R': ramp_te = last_t elif meas.step == 'T': meas.total_time += ramp_te consumer(meas) if meas.test_status not in self._still_testing_steps: return meas.test_status == 'PASS' @proxy.exposed def get_test_program(self, test_number: int) -> HipotProgram: test_program = HipotProgram(**self._get_test_settings( test_number)) # type: ignore test_program.arc_mode = ArcMode[self._get_arc_mode()] test_program.ground_mode = GroundMode[self._get_ground_mode()] k = 'ac' if test_program.is_ac else 'dc' for attr in ('ref_current', 'arc_current'): setattr(test_program, attr, getattr(self, f'_get_{k}_{attr}')()) if test_program.is_ac: _freq: int = self._get_ac_frequency() # type: ignore test_program.frequency = Frequency.from_number(_freq) else: test_program.frequency = Frequency.NONE return test_program @proxy.exposed def run_test_by_number(self, test_number: int, consumer: HIPOT_CONSUMER = None) -> bool: self._clear_errors() self.set_test_program(test_number) test_program = self.get_test_program(test_number) return self.__run_test(test_program, consumer) @proxy.exposed def run_test_by_specification(self, test_program: HipotProgram, consumer: HIPOT_CONSUMER = None) -> bool: test_number = 21 self._clear_errors() self.set_test_program(test_number) self._set_test_type(test_program.test_type.name) self._set_arc_mode(test_program.arc_mode.name) self._set_ground_mode(test_program.ground_mode.name) self._set_ramp_t(test_program.ramp_t) k = 'ac' if test_program.is_ac else 'dc' for attr in ('test_t', 'voltage', 'upper', 'lower', 'ref_current', 'arc_current'): getattr(self, f'_set_{k}_{attr}')(getattr(test_program, attr)) if test_program.is_ac: self._set_ac_frequency(test_program.frequency.to_number()) if not test_program == self.get_test_program(test_number): raise HipotTesterError( f'failed to confirm test programming from spec {test_program}') if self.ERROR_CHECK_ONLY_AFTER_LOAD: self.check_for_error_code('checking after load') return self.__run_test(test_program, consumer) def _instrument_debug(self) -> None: # ac_spec = HipotProgram( # ramp_t=3.0, test_t=1.0, voltage=0.6, upper=1.0, # lower=0.0, ref_current=0.0, arc_current=1.0, test_type=TestType.ACW, # frequency=Frequency.SIXTY, arc_mode=ArcMode.ON_STOP, # ground_mode=GroundMode.ON, is_ac=True, total_t=4.0 # ) dc_spec = HipotProgram(ramp_t=10.0, test_t=3.3, voltage=1.5, upper=0.1, lower=0.0, ref_current=0.0, arc_current=1.0, test_type=TestType.DCW, frequency=Frequency.NONE, arc_mode=ArcMode.ON_STOP, ground_mode=GroundMode.ON, is_ac=False, total_t=13.3) self.run_test_by_number(2) self.run_test_by_specification(dc_spec)
class Cirris(Serial): """ get currently programmed tests as a command_string run specific test and return bool P/F """ # ? W:\TestStation Data Backup\instruments\data\Cirris TestStation Language 2019.3.1.pdf _config = configuration.from_yml( r'W:\Test Data Backup\instruments\config\cirrus.yml') DEVICE_NAME = _config.field(str) BAUDRATE = _config.field(int) TIMEOUT = _config.field(float) RX_ACCUMULATION_TIME_S = _config.field(float) XON_X_OFF = False ENCODING = 'utf-8' TERM_CHAR = '\r\n' _start_test = NoReturn('CHTE({test_prog} {test_type})') _get_status = NoReturn('STAT') _remote_mode = NoReturn('') _fail_sound = ReturnBool('SOUN(3)') _pass_sound = ReturnBool('SOUN(5)') # TODO: do we need to turn the sound off or set its duration? _is_cable_present = ReturnBool('PRES') _calculate_fault_location = ReturnBool('FAUL({do_calculate})') _local_mode = ReturnBool('EXIT') _self_test = ReturnBool('SELF') _delay_test_start = ReturnBool('TDEY({setting})') _list_tests = ReturnStatus('M_LI') def get_status(self) -> Union[bool, str]: self._get_status() rx = self.read() if rx in {'T', 'F'}: return rx == 'T' return rx @proxy.exposed def is_cable_present(self) -> bool: return self._is_cable_present() @proxy.exposed def get_tests(self) -> str: result = self._list_tests() if not result: raise CirrisError('failed to retrieve test list') return result @proxy.exposed def run_test(self, test_program: CirrusTestProgram) -> Union[str, bool]: # TODO: parse failing result string self._start_test(test_prog=test_program.program_number, test_type=test_program.test_type.value) tf = time() + test_program.duration while 1: try: result = self.read() except SerialTimeoutException as e: if time() > tf: raise CirrisError('failed to get test result') from e else: if result == 'T': return True return result @register.after('_instrument_setup') def _cirris_setup(self) -> None: self._remote_mode() if not self._calculate_fault_location(do_calculate=False): raise CirrisError('faild to disable fault location calc') if not self._delay_test_start(setting=False): raise CirrisError('failed to disable test start delay') @register.before('_instrument_cleanup') def _cirris_cleanup(self) -> None: self._local_mode() def _instrument_check(self) -> None: self.get_tests() def _instrument_debug(self) -> None: [self.info(line) for line in self.get_tests().splitlines()]
class LeakTester(TCPIP): _config = configuration.from_yml( r'W:\Test Data Backup\instruments\config\leak_tester.yml') display_name = _config.field(str) IP_ADDRESS = _config.field(str) PORT = _config.field(int) BUFFER_SIZE = _config.field(int) TIMEOUT = _config.field(float) TX_WAIT_S = _config.field(float) PROCESSING_TIME_S = _config.field(float) TEST_PROGRAM_MAP = _config.field(dict) ENCODING = 'utf-8' TERM_CHAR = ']' START_TEST_COMMAND = 'SRP' _test_program_name = Command('SPN%s') _test_program_number = Command('SCP%d') _test_pressure = Command('STP%3.5f') # noinspection SpellCheckingInspection _pressure_max = Command('SPTP%3.5f') # noinspection SpellCheckingInspection _pressure_min = Command('SPTM%3.5f') _fast_fill_timer = Command('ST3%3.1f') _fill_timer = Command('ST4%3.1f') _settle_timer = Command('ST5%3.1f') _test_timer = Command('ST6%3.1f') _vent_timer = Command('ST7%3.1f') _increase_limit = Command('SML%3.5f') _decay_limit = Command('SMD%3.5f') _test_volume = Command('STV%3.5f') _test_type = Command('STT%d') @register.before('__init__') def _leak_test_constants(self) -> None: self.test_program_map = { int(k): v for k, v in self.TEST_PROGRAM_MAP.items() } def _instrument_check(self) -> None: self._test_program_number() def __set_test_program_number(self, test_program_number: int) -> None: self._test_program_number(test_program_number) self._instrument_delay(self.PROCESSING_TIME_S) if self._test_program_number() != test_program_number: raise LeakTesterError( f'failed to set test program to number {test_program_number}') def __run_test(self, test_program: LeakTesterSettings, consumer: LT_CONSUMER = None) -> bool: # type: ignore consumer = consumer if callable(consumer) else self.debug consumer(test_program) # type: ignore self.write(self.START_TEST_COMMAND) while 1: try: line = self.read() except socket.timeout: raise LeakTesterError( 'no message from leak tester during test') else: if not line: continue meas = LeakTesterMeasurement(line) if consumer: consumer(meas) # type: ignore if meas.is_result: return meas.is_pass def __get_program_number_from_model_number(self, mn: int) -> int: if mn not in self.test_program_map: raise LeakTesterError(f'no test program for mn {mn}') return self.test_program_map[mn] @proxy.exposed def get_test_by_number(self, test_number: int) -> LeakTesterSettings: self.__set_test_program_number(test_number) return LeakTesterSettings(**{ k: getattr(self, f'_{k}')() for k in LeakTesterSettings.field_names() }) @proxy.exposed def get_test_from_model_number(self, mn: int) -> LeakTesterSettings: return self.get_test_by_number( self.__get_program_number_from_model_number(mn)) @proxy.exposed def run_test_by_number(self, test_number: int, consumer: LT_CONSUMER = None) -> bool: return self.__run_test(self.get_test_by_number(test_number), consumer) @proxy.exposed def run_test_from_model_number(self, mn: int, consumer: LT_CONSUMER = None) -> bool: return self.run_test_by_number( self.__get_program_number_from_model_number(mn), consumer) @proxy.exposed def run_test_by_specification(self, test_program: LeakTesterSettings, consumer: LT_CONSUMER = None) -> bool: test_number = 21 self.__set_test_program_number(test_number) [ getattr(self, f'_{k}')(getattr(test_program, k)) for k in test_program.field_names() ] self._instrument_delay(self.PROCESSING_TIME_S) new_program = self.get_test_by_number(test_number) if test_program != new_program: raise LeakTesterError( f'failed to confirm test from {test_program} to {new_program}') return self.__run_test(test_program, consumer) def _instrument_debug(self) -> None: self.info(self.get_test_by_number(15))
class BKPowerSupply(VISA, _DCPowerSupply): def calculate_knee(self, percent_of_max: float, num_steps: int, top: float, bottom: float, consumer: Callable[[DCKneeUpdate], None]) -> float: raise NotImplementedError # ? W:\Test Data Backup\test\doc\9200_Series_manual.pdf _config = configuration.from_yml(r'instruments\bk_power_supply.yml') display_name = _config.field(str) PATTERN = _config.field(str) MEASUREMENT_WAIT = _config.field(float) COMMAND_EXECUTION_TIMEOUT = _config.field(float) RAMP_STEPS: Tuple[DCLevel, ...] = ( DCLevel(24., 15.), DCLevel(26., 13.8), DCLevel(28., 12.9), DCLevel(30., 12.), DCLevel(32., 11.3), ) # noinspection SpellCheckingInspection class __Command: RESET = '*RST' SETUP = '*ESE 60;*SRE 48;*CLS' IS_DONE = '*OPC? ' SET_VALUES = 'APPL %.6f,%.6f' GET_VALUES = 'APPL?' SET_OUTPUT = 'OUTP %d' GET_OUTPUT = 'OUTP?' GET_VOLT = ':MEAS:VOLT?' GET_CURR = ':MEAS:CURR?' GET_POW = ':MEAS:POW?' next_meas = 0. def _instrument_check(self) -> None: self.read(self.__Command.GET_VOLT) def __command(self, packet: str) -> None: self.write(packet) command_timeout = self.COMMAND_EXECUTION_TIMEOUT + time() while command_timeout > time(): if self.read(self.__Command.IS_DONE): return raise BKPowerSupplyError(f'failed to confirm command {packet}') @proxy.exposed def send_reset(self): return self.__command(self.__Command.RESET) @register.after('_instrument_setup') def _bk_setup(self): self.__command(self.__Command.SETUP) self.next_meas = time() @register.after('_bk_setup') @register.before('_instrument_cleanup') def _bk_cleanup(self) -> None: self.write_settings(DCLevel(0., 0.), False) @proxy.exposed def read_settings(self) -> Tuple[DCLevel, bool]: return DCLevel(*self.read(self.__Command.GET_VALUES)), \ bool(self.read(self.__Command.GET_OUTPUT)) @proxy.exposed def set_settings(self, dc_level: DCLevel) -> None: self.__command(self.__Command.SET_VALUES % (dc_level.V, dc_level.A)) @proxy.exposed def get_settings(self) -> DCLevel: return DCLevel(*self.read(self.__Command.GET_VALUES)) @proxy.exposed def set_output(self, output_state: bool) -> None: self.__command(self.__Command.SET_OUTPUT % int(cast(bool, output_state))) @proxy.exposed def get_output(self) -> bool: return bool(self.read(self.__Command.GET_OUTPUT)) @proxy.exposed def write_settings(self, dc_level: DCLevel = None, output_state: bool = None): if dc_level is None and output_state is None: raise BKPowerSupplyError( 'must call .write_settings() with at least one arg') was_dc_level_correct = (dc_level is None) or (dc_level == self.get_settings()) if was_dc_level_correct: if dc_level is not None: self.info(f'power settings = {dc_level}') else: self.set_settings(dc_level) was_output_state_correct = ( output_state is None) or not (output_state ^ self.get_output()) if was_output_state_correct: if output_state is not None: self.info(f'output state = {output_state}') else: self.set_output(output_state) error_strings = [] if not was_dc_level_correct: if self.get_settings() != dc_level: error_strings.append(dc_level) else: self.info(f'power settings = {dc_level}') if not was_output_state_correct: if self.get_output() ^ cast(bool, output_state): error_strings.append(f'output_enable={output_state}') else: self.info(f'output state = {output_state}') if error_strings: raise BKPowerSupplyError(f'failed to set to ' + ', '.join(error_strings)) else: self.next_meas = time() + (self.MEASUREMENT_WAIT * 1.5) @proxy.exposed def measure(self, fresh: bool = True): """ note that, even though we send the measure command, rather than the fetch command, the BK appears to be responding with a buffered value updated every 220ms this method returns in ~15ms unless @fresh, in which case it waits for the value to have been updated """ if fresh: self._instrument_delay(self.next_meas - time()) self.next_meas = time() + self.MEASUREMENT_WAIT meas = DCLevel(*list( map(self.read, [self.__Command.GET_VOLT, self.__Command.GET_CURR]))) self.info(meas, f'fresh={fresh}') return meas @proxy.exposed def ramp_up(self): [ self.write_settings(step, output_state=True) for step in self.RAMP_STEPS ] @proxy.exposed def calculate_connection_state(self, calc): return _DCPowerSupply.calculate_connection_state(self, calc) @proxy.exposed def off(self): return _DCPowerSupply.off(self) def _instrument_debug(self) -> None: self.log_level(logging.DEBUG) self.calculate_connection_state(LightLineV1ConnectionState) self.ramp_up() self.measure(fresh=True) self.write_settings(DCLevel(0., 0.)) tf = time() + 5 while time() < tf: self.measure(fresh=True)
class Assure(HackScript): bk = TestInstrument(BKPowerSupply(), logging.INFO) lm = TestInstrument(LightMeter(), logging.INFO) ftdi = TestInstrument(RS485(), logging.DEBUG) nfc = TestInstrument(NFC(), logging.INFO) _config = configuration.from_yml('pixie_hack/.yml') ASSOCIATION_TABLE_PATH = _config.field(str) SHIPMENT_ASSOCIATION_TABLE_PATH = _config.field(str) AFTER_ERASE_WAIT_S = _config.field(float) psu_label_pattern = re.compile(r'(?i)\[PSU#\|WDPB:001-(\d{4})]') dut_label_pattern = re.compile(r'(?i)\[PIXIE2PT0:(\d{4})]') def __init__(self): super().__init__() self.AFTER_ERASE_WAIT_S = int(self.AFTER_ERASE_WAIT_S) self.num_steps = int(self.AFTER_ERASE_WAIT_S * 10) def get_result_from_id(self, dut_id: int) -> bool: with open(self.ASSOCIATION_TABLE_PATH, newline='') as rf: return { int(row['dut_id']): row['test_pf'].upper() == 'TRUE' for row in csv.DictReader(rf) }.get(dut_id, None) def associate_id_with_shipment_label(self, dut_id: int) -> int: with open(self.SHIPMENT_ASSOCIATION_TABLE_PATH, newline='') as rf: table = { int(row['dut_id']): int(row['shipment_id']) for row in csv.DictReader(rf) } shipment_id = table.get(dut_id, None) if shipment_id is not None: return shipment_id if not table: table[dut_id] = 1 else: table[dut_id] = max(table.values()) + 1 with open(self.SHIPMENT_ASSOCIATION_TABLE_PATH, 'w+', newline='') as wf: writer = csv.DictWriter(wf, ['dut_id', 'shipment_id']) writer.writeheader() writer.writerows( [dict(dut_id=k, shipment_id=v) for k, v in table.items()]) return table[dut_id] def __call__(self) -> None: try: while 1: user_input = input('scan white DUT label -> ') parsed = self.dut_label_pattern.findall(user_input) if not parsed: print('scan invalid') continue dut_id = int(parsed[0]) if not self.get_result_from_id(dut_id): print(f'DUT #{dut_id} did not pass the light test.') continue shipment_id = self.associate_id_with_shipment_label(dut_id) user_input = input( f'apply power to light, then scan black PSU label #{shipment_id} -> ' ) if user_input.upper() != f'[PSU#|WDPB:001-{shipment_id:04d}]': print('scan invalid') continue print('\nwaiting for startup...\n') self.progress = progressbar.ProgressBar(maxval=self.num_steps) self.progress.start() for i in range(1, self.num_steps + 1): sleep(.1) self.progress.update(i) print() print('\nerasing test firmware...\n') self.progress = progressbar.ProgressBar(maxval=5) self.progress.start() for i in range(1, 6): self.ftdi.ser.send_ascii('U') sleep(1.) self.progress.update(i) print() print(f'\nwhite #{dut_id} / black #{shipment_id} done.\n') except KeyboardInterrupt: pass