1b3a65823851f8ad28d214ad005765bf6b9805480David Rochberg#!/usr/bin/python 2b341239ef136bea7884b0d44e70879cd7d8a3d21Byron Kubert# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 3b3a65823851f8ad28d214ad005765bf6b9805480David Rochberg# Use of this source code is governed by a BSD-style license that can be 4b3a65823851f8ad28d214ad005765bf6b9805480David Rochberg# found in the LICENSE file. 5b3a65823851f8ad28d214ad005765bf6b9805480David Rochberg 6d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubertimport cellular_system_error 7d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubertimport cellular_logging 8d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubertimport os 9b341239ef136bea7884b0d44e70879cd7d8a3d21Byron Kubertimport select 10b3a65823851f8ad28d214ad005765bf6b9805480David Rochbergimport socket 11d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubertimport traceback 12b3a65823851f8ad28d214ad005765bf6b9805480David Rochberg 13b3a65823851f8ad28d214ad005765bf6b9805480David Rochberg 14b3a65823851f8ad28d214ad005765bf6b9805480David Rochbergclass PrologixScpiDriver: 15d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan """Wrapper for a Prologix TCP<->GPIB bridge. 16d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan http://prologix.biz/gpib-ethernet-controller.html 17d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan http://prologix.biz/index.php?dispatch=attachments.getfile&attachment_id=1 18d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 19d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan Communication is over a plain TCP stream on port 1234. Commands to 20d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan the bridge are in-band, prefixed with ++. 21d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 22d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan Notable instance variables include: 23d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 24d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.auto: When 1, the bridge automatically addresses the target 25d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan in listen mode. When 0, we must issue a ++read after every 26d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan query. As of Aug '11, something between us and the Agilent 8960 27d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan is wrong such that running in auto=0 mode leaves us hanging if 28d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan we issue '*RST;*OPC?' 29b3a65823851f8ad28d214ad005765bf6b9805480David Rochberg """ 30d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert all_open_connections = {} 31d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 32d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan def __init__(self, hostname, port=1234, gpib_address=14, 33d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan read_timeout_seconds=30, connect_timeout_seconds=5): 34d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan """Constructs a wrapper for the Prologix TCP<->GPIB bridge : 35d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan Arguments: 36d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan hostname: hostname of prologix device 37d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan port: port number 38d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan gpib_address: initial GPIB device to connect to 39d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan read_timeout_seconds: the read time out for the socket to the 40d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan prologix box 41d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan connect_timeout_seconds: the read time out for the socket to the 42d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan prologix box 43d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan """ 44d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert logger_name = 'prologix' 45d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan s = 'IP:%s GPIB:%s: ' % (hostname, gpib_address) 46d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert formatter_string = '%(asctime)s %(filename)s %(lineno)d ' + s + \ 47d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert '- %(message)s' 48d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.scpi_logger = cellular_logging.SetupCellularLogging( 49d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert logger_name, formatter_string) 50d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert 51d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.connection_key = "%s:%s" % (hostname, port) 52d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.connection_data = {self.connection_key: traceback.format_stack()} 53d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert if self.connection_key in self.all_open_connections.keys(): 54d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert raise cellular_system_error.BadState( 55d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert 'IP network connection to ' 56d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert 'prologix is already in use. : %s ' % self.all_open_connections) 57d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.all_open_connections[self.connection_key] = self.connection_data 58d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.socket = connect_to_port(hostname, port, connect_timeout_seconds) 59d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.read_timeout_seconds = read_timeout_seconds 60d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.socket.setblocking(0) 61d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.SetAuto(1) 62d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self._AddCarrigeReturnsToResponses() 63d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.SetGpibAddress(gpib_address) 64d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.scpi_logger.debug('set read_timeout_seconds: %s ' % 65d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.read_timeout_seconds) 66d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 67d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan def __del__(self): 68d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.Close() 69d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 70d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert def _AddCarrigeReturnsToResponses(self): 71d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert """ 72d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert Have the prologix box add a line feed to each response. 73d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert Some instruments may need this. 74d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert """ 75d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert pass 76d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.Send('++eot_enable 1') 77d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.Send('++eot_char 10') 78d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 79d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan def SetAuto(self, auto): 80d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan """Controls Prologix read-after-write (aka 'auto') mode.""" 81d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan # Must be an int so we can send it as an arg to ++auto. 82d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.auto = int(auto) 83d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.Send('++auto %d' % self.auto) 84d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 85d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan def Close(self): 86d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan """Closes the socket.""" 87d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan try: 88d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.scpi_logger.error('Closing prologix devices at : %s ' % 89d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.connection_key) 90d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.all_open_connections.pop(self.connection_key) 91d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert except KeyError: 92d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.scpi_logger.error('Closed %s more then once' % 93d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.connection_key) 94d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert try: 95d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.socket.close() 96d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan except AttributeError: # Maybe we close before we finish building. 97d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan pass 98d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 99d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan def SetGpibAddress(self, gpib_address): 100d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan max_tries = 10 101d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan while max_tries > 0: 102d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan max_tries -= 1 103d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.Send('++addr %s' % gpib_address) 104d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan read_back_value = self._DirectQuery('++addr') 105d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan try: 106d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert if int(read_back_value) == int(gpib_address): 107d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan break 108d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan except ValueError: 109d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan # If we read a string, don't raise, just try again. 110d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan pass 111d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.scpi_logger.error('Set gpib addr to: %s, read back: %s' % 112d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan (gpib_address, read_back_value)) 113d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.scpi_logger.error('Setting the GPIB address failed. ' + 114d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 'Trying again...') 115d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 116d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan def Send(self, command): 117d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.scpi_logger.info('] %s', command) 118d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan try: 119d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.socket.send(command + '\n') 120d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan except Exception as e: 121d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.scpi_logger.error('sending SCPI command %s failed. ' % 122d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan command) 123d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.scpi_logger.exception(e) 124d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert raise SystemError('Sending SCPI command failed. ' 125d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert 'Did the instrument stopped talking?') 126d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 127d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan def Reset(self): 128d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan """Sends a standard SCPI reset and waits for it to complete.""" 129d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan # There is some misinteraction between the devices such that if we 130d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan # send *RST and *OPC? and then manually query with ++read, 131d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan # occasionally that ++read doesn't come back. We currently depend 132d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan # on self.Query to turn on Prologix auto mode to avoid this 133d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.Send('*RST') 134d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.Query('*OPC?') 135d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 136d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan def Read(self): 137d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan """Read a response from the bridge.""" 138d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan try: 139d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan ready = select.select([self.socket], [], [], 140d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.read_timeout_seconds) 141d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan except Exception as e: 142d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.scpi_logger.exception(e) 143d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan s = 'Read from the instrument failed. Timeout:%s' % \ 144d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.read_timeout_seconds 145d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.scpi_logger.error(s) 146d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan raise SystemError(s) 147d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 148d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan if ready[0]: 149d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan response = self.socket.recv(4096) 150d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan response = response.rstrip() 151d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.scpi_logger.info('[ %s', response) 152d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan return response 153d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan else: 154d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.Close() 155d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan s = 'Connection to the prologix adapter worked.' \ 156d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 'But there was not data to read from the instrument.' \ 157d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 'Does that command return a result?' \ 158d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 'Bad GPIB port number, or timeout too short?' 159630f29391fabb13a6e6d191fa747e81512bcb726Byron Kubert raise cellular_system_error.InstrumentTimeout(s) 160d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 161d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan def Query(self, command): 162d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan """Send a GPIB command and return the response.""" 163d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan #self.SetAuto(1) #maybe useful? 164d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert 165d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert s = list(self.scpi_logger.findCaller()) 166d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert s[0] = os.path.basename(s[0]) 167d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert 168d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert s = list(self.scpi_logger.findCaller()) 169d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert s[0] = os.path.basename(s[0]) 170d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert self.scpi_logger.debug('caller :' + str(s) + command) 171d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert 172d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.Send(command) 173d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan if not self.auto: 174d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.Send('++read eoi') 175d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan output = self.Read() 176d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan #self.SetAuto(0) #maybe useful? 177d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan return output 178d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 179d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan def _DirectQuery(self, command): 180d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan """Sends a query to the prologix (do not send ++read). 181d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan 182d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan Returns: response of the query. 183d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan """ 184d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan self.Send(command) 185d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan return self.Read() 186b3a65823851f8ad28d214ad005765bf6b9805480David Rochberg 187b3a65823851f8ad28d214ad005765bf6b9805480David Rochberg 188b341239ef136bea7884b0d44e70879cd7d8a3d21Byron Kubertdef connect_to_port(hostname, port, connect_timeout_seconds): 189d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan # Right out of the python documentation, 190d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan # http://docs.python.org/library/socket.html 191d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan for res in socket.getaddrinfo( 192d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): 193d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan af, socktype, proto, _, sa = res 194d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan try: 195d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan s = socket.socket(af, socktype, proto) 196d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan except socket.error as msg: 197d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert raise cellular_system_error.SocketTimeout( 198d55c96d6bef22b7fc1d815ab83caec4935ba76f2Byron Kubert 'Failed to make a new socket object. ' + str(msg)) 199d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan try: 200d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan s.settimeout(connect_timeout_seconds) 201d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan s.connect(sa) 202d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan except socket.error as msg: 203d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan try: 204d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan s.close() 205d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan except Exception: 206d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan pass # Try to close it, but it may not have been created. 207d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan temp_string_var = ' Could be bad IP address. Tried: %s : %s' % \ 208d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan (hostname, port) 209630f29391fabb13a6e6d191fa747e81512bcb726Byron Kubert raise cellular_system_error.SocketTimeout(str(msg) + 210630f29391fabb13a6e6d191fa747e81512bcb726Byron Kubert temp_string_var) 211d34edf26ab8171d0ad944293d2a0439fa1f1b1b4Ben Chan return s 212