Source code for hardware_interfaces.fpas

from typing import List, Tuple
import logging
import time
import json
import numpy as np
from numpy import ndarray

# Import essential modules
import serial

# Set up logger
logger = logging.getLogger(__name__)


[docs]class Fpas: """ This class is used to communicate with the FPAS Box from Infrared Systems Development corporation. We use Pyserial to communicate with the FPAS electronics. The FPAS is used to amplify the signal which comes from MCT detector and then is send to the ADC. Please note that all the values should be in integer format. The algorithm will tell you that something is wrong if you plug in a float value. All serial communication to and from the FPAS2 electronics occurs at 9600 baud rate, with 8 data bits, 1 stop bit, no parity bits, and no termination characters, and no flow control. All commands must begin with the character ‘I’ (Hex 0x49). All communication occurs in 8 bytes. IXXXXXXX. Where XXXXXXX is specific to certain commands listed below. Args: port (str): COM port to which the FPAS is connected. Please check in the device manager of your operating system. * E.g.: "port5" integration_time (int): Integration time in nanoseconds. This is the integration time that is set upon init. Defaults to 3014 ns. integration_delay (int): Integration delay in nanoseconds. This is the integration delay that is set upon init. Defaults to 100 ns. json_path (str): Path to json file with all gain and trim values. If this is specified, all trims and gains will be set upon init. Default to None. name (str, optional): Name / Identifier to give to this FPAS. This is relevant for log statements, especially when there is more than one FPAS in the setup. Defaults to "FPAS". Attributes: ser (serial.Serial): pyserial object which initiates the communication with the FPAS. number_of_channel (int): Number of channels for our FPAS. From 0 to 144 for this FPAS. Other FPAS have up to 255. min_gain (int): Minimum value for gain is 0. max_gain (int): Maximum value for gain is 7. min_trim (int): Minimum value for gain is 0. max_trim (int): Maximum value for gain is 255. step_delay_time (int): Stepsize in which the delay time/ width can be set. 5 nanoseconds. min_delay_time (int): Minimum value for the delay time is 50 nanoseconds. max_delay_time (int): Maximum value for the delay time is 1325 nanoseconds. step_int_time (int): Stepsize in which the integration time can be set. 20 nanoseconds. min_int_time (int): Minimum value for the delay time is 54 nanoseconds. max_int_time (int): Maximum value for the integration time is 5154 nanoseconds. name = name (str): Name / Identifier to give to this FPAS. This is relevant for log statements, especially when there is more than one FPAS in the setup. Defaults to "FPAS". References: | FPAS2 Serial Communication Guide | Emails with Walter """ def __init__( self, port: str, integration_time: int = 3014, integration_delay: int = 100, json_path: str = None, name: str = "FPAS", ): #! Hard-coded parameters of our FPAS self.number_of_channel = 144 # From 0 to 144 for this FPAS. # Limit values for gain self.min_gain = 0 self.max_gain = 7 # Limit values for trim self.min_trim = 0 self.max_trim = 255 # Limit values for delay time all in nanoseconds self.step_delay_time = 5 # in ns self.min_delay_time = 50 # in ns self.max_delay_time = 1325 # in ns # Limit values for integration time all in nanoseconds self.step_int_time = 20 # in ns self.min_int_time = 54 # in ns self.max_int_time = 5154 # in ns # Assigning attributes self.name = name # Connecting the controller with pyserial self.ser = serial.Serial( port, 9600, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=1, ) logger.info('Port is open."{}" is ready.'.format(self.name)) # Set default integration time and delay self.integration_time = integration_time self.integration_delay = integration_delay self.set_delay_and_integration_time(integration_time, integration_delay) # If path is specified, set all gains and trims if json_path: self.json_path = json_path gains, trims = self.upload(json_path) self.set_gain_all(gains) self.set_trim_all(trims)
[docs] def read(self): """ Reads out the FPAS answer to a sent command. Since only one command ("ICXYYY00") is used for reading out data we can take the last three string characters and transform them into an integer to get the gain/ trim value for a specific channel. Returns: int: Answer to sent command. Typically consists of command and value. """ readout = self.ser.readline() # The only readout function is ICXYYY00. It returns ICXYYZZZ # where ZZZ is the gain/trim value at the particular channel. We # are only interested in the last three characters transform # them into int to get rid of zeros (i.e. 001 which corresponds # to 1) readout = int(readout.decode("utf-8")[-3:]) readout = readout.decode("utf-8")[-3:] return readout
[docs] def send_command(self, input_command: str): """ Creates necessary ascii format of the command which will be send to the FPAS. Raises an error if the input_command contains any dots since the FPAS only accepts integer values. Args: input_command (str): Command which should be send to the FPAS Returns: str: Command which is send to FPAS. In ascii format. """ # FPAS only takes integers as setting parameters (i.e. gain). This makes sure that no float appears if "." in input_command: raise ValueError( "The command <<{}>> you wanted to send contains a float. Only int is acceptable.".format( input_command ) ) # .encode is used to transform command into byte # All commands must begin with the character ‘I’ (Hex 0x49). ascii_command = (str(input_command)).encode() # Actual send of command with pyserial self.ser.write(ascii_command)
def __check_limits(self, value: int, min_value: int, max_value: int, name: str): """ Checks if the given value lies between the minimal and maximal value given. Sets value to the limit if the value exceeds it. Args: value (int): Value which should be checked. min_value (int): Lower limit the value is allowed to have. max_value (int): Upper limit the value is allowed to have. name (int): Since this function is general for any values we pass it, we have do define a name for logging. Retruns: int: Returns the value if between limits, returns max_value if value too high, returns min_value if value too low """ # Check if it is below the minimum value. if true set value to # min value if value < min_value: logger.info( "Value for {} set below {}. Setting {} to {}.".format( name, min_value, name, min_value ) ) return min_value # Check if it is above the maximum value. if true set value to # max value elif value > max_value: return max_value logger.info( "Value for {} set above {}. Setting {} to {}.".format( name, max_value, name, max_value ) ) # Everything is fine? Value in between max and min? then fine: else: return value def __trail_zeros(self, number_of_chars: int, string: str): """ Trails/ Adds zeros to the beginning of the string if value is too small. All communication occurs in 8 bytes. IXXXXXXX. Where XXXXXXX is specific to certain commands. Thereby, the values we want to set are sometimes to small to fill the commands to the needed 8 bytes. E.g. trim = 5 but the comand needs three characters (005) -> we put zeros (as strings since its all strings) in front of the 5 to fill the command. Args: number_of_chars (int): Defines how long the value should be. I.e. trim should have three characters. (i.e. 004) string (str): This is the value that we want to set. * E.g. trim = 4 Returns: str: Returns a "zero-padded" string where the zeros come before the value character. i.e. 004, 064, 145 """ number_of_trails = number_of_chars - len(string) # The string/ value should not be too long. Exceeding the # commands length also does not work, duh! if number_of_trails < 0: logger.error("The string is too large for the command you wanted to send.") # trail zeros on "the left" of the string until the string is # number_of_chars long No trailing of zeros is necessary if the # string is already long enough. rjust does that for you string = string.rjust(number_of_chars, "0") return string def __time_to_command(self, step_size: int, time_value: int, min_time_value): """ Translates/ Transforms the delay time or integration time (in ns) to the correspoding str which the FPAS understands. The integration time and delay time can only be set in certain steps. For integration time its steps of 20 ns starting at 54 ns to 5154 ns. The delay time can be changed in steps of 5 ns from 50 ns to 1325 ns. Args: step_size (int): Step size with which the delay or integration time can be changed. time_value (int): The value which we want to set. i.e. 200 ns delay time min_time_value (int): Minimal allowed value for the time delay. i.e. delay time min = 50 """ # This calculates the integer number we are able to send to the # FPAS with a given integration value e.g. User wants to set # integration time of 100 ns. AiAiAi have you really read the # documentation?? But we can only set the integration time in # steps of 20 ns 54(min)+20+20 = 94 (equal sending 002) or 94+20 # = 114 (= 003). So we calculate 100 - 54 / 20 = 2.3 which we # round (in the way we learned in elementary school) and obtain # 2. Therefore we send 002 which is equivalent to 94 string = round((time_value - min_time_value) / step_size) return string
[docs] def readout_channels(self, channels: list = list(range(144)), gain: bool = True): """ Reads out gain or trim values for specified channels and returns a dict. Default are all channel for an FPAS with 144 channels. Args: channels (list, optional): List of channels which should be read out after each other. Default are list from 0...144 channels (all channels for FPAS in H-Lab). gain (bool, optional): Defines if gain or trim values should be read out. Defaults to true = gain value should be read out. Returns: dict: Dictionary containing either {channel: trim_values} or {channel: gain_values}. """ values = {} for channel in channels: if gain == True: logger.info( """Reading out channel gain value for {}.""".format(self.name) ) values[channel] = self.read_gain_trim(channel, gain=True) elif gain == False: logger.info( """Reading out channel trim value for {}.""".format(self.name) ) values[channel] = self.read_gain_trim(channel, gain=False) return values
[docs] def set_gain_all(self, values: dict): """ Set gain for all channels. Channels and gains are taken from a dictionary. This method is implemented since the FPAS in the H-Lab is not able to set all gain value for all channels at the same time due to old firmware (1.4) Args: values (dict): Dictionary containing channels as keys and gain values as values. * E.g. {0: 3, 1: 7, 2: 5,...} """ logger.info( """Setting gain for all (defined) channels for {}.""".format(self.name) ) for channel, value in values.items(): logger.info( """Setting gain of {} for channel {} to {}.""".format( self.name, channel, value ) ) self.set_gain(int(channel), int(value))
[docs] def set_trim_all(self, values: dict): """ Set trim for all channels. Channels and trims are taken from a dictionary. This method is implemented since the FPAS in the H-Lab is not able to set all trim value for all channels at the same time due to old firmware (1.4) Args: values (dict): Dictionary containing channels as keys and trim values as values. * E.g. {0: 31, 1: 74, 2: 54,...} """ logger.info( """Setting trim for all (defined) channels for {}.""".format(self.name) ) for channel, value in values.items(): logger.info( """Setting trim of {} for channel {} to {}.""".format( self.name, channel, value ) ) self.set_trim(int(channel), int(value))
[docs] def download(self, path: str, gain_values: dict, trim_values: dict): """ Creates a json file containing the gain and trim value for each channel/ pixel. Args: path (str): Path where the json file is saved. Also defines the file name. gain_values (dict): Dictionary containing channels as keys and gain values as values. * E.g. {0: 3, 1: 7, 2: 5,...} trim_values (dict): Dictionary containing channels as keys and trim values as values. * E.g. {0: 31, 1: 74, 2: 54,...} """ gain_trim_values = {} for pixel in gain_values.keys(): gain_trim_values[pixel] = {} gain_trim_values[pixel]["gain"] = gain_values[pixel] gain_trim_values[pixel]["trim"] = trim_values[pixel] logger.info( """Downloading gain and trim values for all channels from json file for {}.""".format( self.name ) ) with open(path, "w", encoding="utf-8") as f: json.dump(gain_trim_values, f, ensure_ascii=False, indent=4)
[docs] def upload(self, path: str): """ Loads gain and trim values from a specific json file and saves them to separate dictionaries. Args: path (str): Path where the json file is located. Returns: dict: Dictionary containing the gain values and channels/ pixels. {pixel0: gain0, pixel1: gain1,...} dict: Dictionary containing the trim values and channels/ pixels. {pixel0: trim0, pixel1: trim1,...} """ logger.info( """Uploading gain and trim values for all channels from json file for {}.""".format( self.name ) ) gain_values = {} trim_values = {} with open(path) as json_file: gain_trim_dict = json.load(json_file) for key, value in gain_trim_dict.items(): gain_values[key] = gain_trim_dict[key]["gain"] trim_values[key] = gain_trim_dict[key]["trim"] return gain_values, trim_values
[docs] def set_gain(self, channel_number: int, gain: int): """ Set gain value for certain channel. **IGXXX00Y**. Modifying Gain values. * G - Indicates modification of the single channel Gain value. * XXX – These three bytes are used to indicate the channel whose gain you are changing. Please ensure that ASCII representation of the numbers is being used. 0 is Hex 0x30 up to 9 which is Hex 0x39. This value can range from 0 to 255, inclusive. * 00 – These two bytes must be 0 (Hex 0x30) * Y – The desired Gain value of the channel. Values can range from 0 (Hex 0x30) to 7 (Hex 0x37), inclusive. 0 is maximum gain and 7 is minimum gain. * E.g.: IG001001 – Set Channel 1 Gain to 1 Please note that in the FPAS2SerCmd.VI, 7 represents maximum gain and 0 represents minimum gain. Note: 0 is maximum gain and 7 is minimum gain. This is opposite to LabView. E.g. set_gain(10,0) wil set the gain of channel 10 to 7 in terms of LabView. set_gain(10,3) will set the gain of channel 10 to 4. Args: channel_number (int): Number of the channel for which we want to set the gain. gain (int): Value for the gain. Between 0 and 7. """ gain = self.__check_limits(gain, self.min_gain, self.max_gain, "gain") channel_number = self.__check_limits( channel_number, 0, self.number_of_channel, "channel" ) channel_number = self.__trail_zeros(3, str(channel_number)) command = "IG" + channel_number + "00" + str(gain) logger.info( """Setting gain value to {} for channel {} for {}.""".format( gain, channel_number, self.name ) ) self.send_command(command) self.ser.flushOutput() time.sleep(0.05)
[docs] def set_gain_all_newfirmware(self, gain: int): """ Set gain value for all channels. Does only work for FPAS version 1.7, not 1.4 (ours) **IA00000N** where N is the gain value 0 - 7 in ascii 00000 – These three bytes must be set to 000 (0x30 0x30 0x30 0x30 0x30). Since all channels are being changed you do not need to indicate a desired channel. * E.g.: IA000007 - Set all channel gain to 7 Args: gain (int): Value for the gain. Between 0 and 7 """ gain = self.__check_limits(gain, self.min_gain, self.max_gain, "gain") command = "IA" + "00000" + str(gain) logger.info( """Setting gain value to {} for all channels for {}.""".format( gain, self.name ) ) self.send_command(command) self.ser.flushOutput() time.sleep(0.05)
[docs] def set_gain_range(self, low: bool = True): """ Modifying the systems gain range. **IL00000Y** * L – Indicates a change in the gain range. * 00000 – These five bytes must be set to 00000 (0x30 0x30 0x30 0x30 0x30). * Y - This byte is used to indicate the gain range. 1 (Hex 0x31) is used for high gain, and 0 (Hex 0x30) is used for low gain. No other values are valid. * E.g.: IL000001 – Set System Gain Range To 1. Args: low (bool, optional): True if gain should be set to low. False if gain should be set high. Defaults to True. """ if low == True: gain = 0 logger.info("""Setting gain range to low for {}.""".format(self.name)) elif low == False: gain = 1 logger.info("""Setting gain range to high for {}.""".format(self.name)) command = "IL" + "00000" + str(gain) # Command for set gain range self.send_command(command) self.ser.flushOutput() time.sleep(0.05)
[docs] def set_trim(self, channel_number: int, trim: int): """ Modifying single channel trim value **ITXXXYYY** * T – Indicates modification of single channel Trim values. * XXX – These three bytes are used to indicate the channel whose trim you are changing. Please ensure that ASCII representation of the numbers is being used. 0 is Hex 0x30 up to 9 which is Hex 0x39. This value can range from 0 to 255, inclusive. * YYY- These three bytes are used to indicate the desired trim value. Please ensure that ASCII representation of the numbers is being used. 0 is Hex 0x30 up to 9 which is Hex 0x39. This value can range from 000 to 255, inclusive. * E.g.: IT001001 – Set Channel 1 Trim to 1. Information comes from other FPAS documentation * Maximum value: 255, minimum value: 0 * IMPORTANT: Always set trim before gain! Args: channel_number (int): Number of the channel for which we want to set the gain. trim (int): Value for the trim. Between 0 and 255. """ trim = self.__check_limits(trim, self.min_trim, self.max_trim, "trim") channel_number = self.__check_limits( channel_number, 0, self.number_of_channel, "channel" ) channel_number = self.__trail_zeros(3, str(channel_number)) trim = self.__trail_zeros(3, str(trim)) command = "IT" + channel_number + trim logger.info( """Setting trim value to {} for channel {} for {}.""".format( trim, channel_number, self.name ) ) self.send_command(command) self.ser.flushOutput() time.sleep(0.05)
[docs] def set_trim_all_newfirmware(self, trim: int): """ Modify all channels trim values. Does only work for FPAS version 1.7, not 1.4 (ours) **II000YYY** * T – Indicates modification of single channel Trim values. * 000 – These three bytes must be set to 000 (0x30 0x30 0x30). Since all channels are being changed, you do not need to indicate a desired channel. * YYY- These three bytes are used to indicate the desired trim value. Please ensure that ASCII representation of the numbers is being used. 0 is Hex 0x30 up to 9 which is Hex 0x39. This value can range from 000 to 255, inclusive. * E.g.: II000001 – Set all Channels' Trim to 1. Args: trim (int): Value for the trim. Between 0 and 255. """ trim = self.__check_limits(trim, self.min_trim, self.max_trim, "trim") trim = self.__trail_zeros(3, str(trim)) command = "II" + "000" + trim logger.info( """Setting trim value to {} for all channels for {}.""".format( trim, self.name ) ) self.send_command(command) self.ser.flushOutput() time.sleep(0.05)
[docs] def set_delay_and_integration_time(self, integration_time: int, delay_time: int): """ Modifying the integration time and delay time. **IWXXXYYY** * XXX- indicates the delay time. Sending 000 (Hex 0x30 0x30 0x30) will set the delay time to 50 ns. Sending 001 (Hex 0x30 0x30 0x31) will set the integration time to 55 ns. And so forth until you send 255 (Hex 0x32 0x35 0x35) which would set the delay time to 1325 ns. This value can range from 000 to 255. * YYY – indicates the integration time. Sending 000 (Hex 0x30 0x30 0x30) will set the integration time to 54 ns. Sending 001 (Hex 0x30 0x30 0x31) will set the integration time to 74 ns. And so forth until you send 255 (Hex 0x32 0x35 0x35) which would set the integration time to 5154 ns. This value can range from 000 to 255. * E.g.: IW255255 – Set Integration Time to Max and Delay Time to Max. Args: integration_time (int): Value for the integration time in nanoseconds. Value should start at 54 ns and chosen in steps of 20 ns. However, the algorithm corrects that if you did not read the docmentation. delay_time (int): Value for the delay time in nanoseconds. Value should start at 50 ns and chosen in steps of 5 ns. """ # Check if the integration time is within limits to begin with integration_time = self.__check_limits( integration_time, self.min_int_time, self.max_int_time, "integration time" ) # Transform the time in nanoseconds to the corresponding string # number i.e. 54 ns = 0, 74 ns = 1 integration_time = self.__time_to_command( self.step_int_time, integration_time, self.min_int_time ) # Trail/ Padd with zeros i.e. 74 ns -> 1 -> 001 integration_time = self.__trail_zeros(3, str(integration_time)) # Analogously for delay time delay_time = self.__check_limits( delay_time, self.min_delay_time, self.max_delay_time, "delay time" ) delay_time = self.__time_to_command( self.step_delay_time, delay_time, self.min_delay_time ) delay_time = self.__trail_zeros(3, str(delay_time)) logger.info( """Setting delay time to {} and integration time to {} for {}.""".format( delay_time, integration_time, self.name ) ) command = "IW" + delay_time + integration_time self.send_command(command) self.ser.flushOutput() time.sleep(0.05)
[docs] def read_integration_time(self): """ Reading integration time value. **ICW00000** is the command which has to be sent. """ command = "ICW" + "00000" logger.info("""Reading out integration time for {}.""".format(self.name)) self.send_command(command) time.sleep(0.05) readout = self.read() return readout
[docs] def read_integration_delay(self): """ Reading integration time value. **ICD00000** is the command which has to be sent. """ command = "ICD" + "00000" logger.info("""Reading out integration delay for {}.""".format(self.name)) self.send_command(command) time.sleep(0.05) readout = self.read() return readout
[docs] def read_gain_trim(self, channel_number: int, gain: bool = True): """ Reading gain/ trim value. **ICXYYY00** * X – The read function. 'G' (Hex 0x47) indicates a Read Gain function. 'T' (Hex 0x54) indicates a Read Trim Function. * YYY – These three bytes are used to indicate the desired channel. Please ensure that ASCII representation of the numbers is being used. 0 is Hex 0x30 up to 9 which is Hex 0x39. This value can range from 000 to 255, inclusive. * E.g.: ICG00100 – Read Gain Value for Channel 1. * Returned: ICXYYZZZ – ZZZ is the gain/trim value at the particular channel. Note: Please note, firmware version 1.4 or higher is required to reading the gain and trim values Args: channel_number (int): Number of the channel for which we want to set the gain. gain (bool, optional): True if gain should be read out, Flase if trim should be read out Defaults to True. """ # Read out gain if True if gain == True: gain_trim = "G" # Read out trim if True else: gain_trim = "T" # Check if channel in limits and trail with zeros if necessary channel_number = self.__check_limits( channel_number, 0, self.number_of_channel, "channel" ) channel_number = self.__trail_zeros(3, str(channel_number)) command = "IC" + gain_trim + channel_number + "00" logger.info( """Reading out value of channel {} for {}.""".format( channel_number, self.name ) ) self.send_command(command) time.sleep(0.05) readout = self.read() return readout
[docs] def __str__(self): """ Read out all relevant parameters. """ config_information = """ Identifier: {} Number of channels: {} Minimal gain: {} Maximal gain: {} Minimal trim: {} Maximal trim: {} Minimal delay time: {} in ns Maximal delay time: {} in ns Stepsize of delay time: {} in ns Minimal integration time: {} in ns Maximal integration time: {} in ns Stepsize of integration time: {} in ns """.format( self.name, self.number_of_channel, self.min_gain, self.max_gain, self.min_trim, self.max_trim, self.min_delay_time, self.max_delay_time, self.step_delay_time, self.min_int_time, self.max_int_time, self.step_int_time, ) return config_information
def __enter__(self): return self def __exit__(self, type, value, tb): self.end()
[docs] def end(self): """ Closes the serial connection. """ self.ser.close()
if __name__ == "__main__": # logging.basicConfig(level=logging.info) port = "COM25" # path = r"C:\Users\hlab\Desktop\fpas_test1.json" with Fpas(port) as fpas: # g, v = fpas.upload(path) # # for pixel in range(144): # # if pixel <= 133: # # g[str(pixel)] = 0 #Highest gain # # else: # # g[str(pixel)] = 7 #Lowest gain # fpas.set_trim_all(v) # fpas.set_gain_all(g) # gain_vals = fpas.readout_channels() # trim_vals = fpas.readout_channels(gain=False) # fpas.download(path, gain_vals, trim_vals) # Read integration time and delay time integration_time = fpas.read_integration_time() integration_delay = fpas.read_integration_delay() print("Integration time:", integration_time) print("Integration delay:", integration_delay)