Exemple #1
0
def setup_sw_ao(lines, expected_range, task_name='sw_ao'):
    # TODO: DAQmxSetAOTermCfg
    task = create_task(task_name)
    lb, ub = expected_range
    mx.DAQmxCreateAOVoltageChan(task, lines, '', lb, ub, mx.DAQmx_Val_Volts, '')
    mx.DAQmxTaskControl(task, mx.DAQmx_Val_Task_Reserve)
    return task
Exemple #2
0
    def configure_pulser_task(self):
        """ Clear pulser task and set to current settings.

        @return:
        """
        a_channels = [self.channel_map[k] for k in self.a_names]
        d_channels = [self.channel_map[k] for k in self.d_names]

        # clear task
        daq.DAQmxClearTask(self.pulser_task)

        # add channels
        if len(a_channels) > 0:
            print(self.a_names, a_channels)
            daq.DAQmxCreateAOVoltageChan(self.pulser_task,
                                         ', '.join(a_channels),
                                         ', '.join(self.a_names),
                                         self.min_volts, self.max_volts,
                                         daq.DAQmx_Val_Volts, '')

        if len(d_channels) > 0:
            print(self.d_names, d_channels)
            daq.DAQmxCreateDOChan(self.pulser_task, ', '.join(d_channels),
                                  ', '.join(self.d_names),
                                  daq.DAQmx_Val_ChanForAllLines)

            # set sampling frequency
            daq.DAQmxCfgSampClkTiming(self.pulser_task, 'OnboardClock',
                                      self.sample_rate, daq.DAQmx_Val_Rising,
                                      daq.DAQmx_Val_ContSamps,
                                      10 * self.sample_rate)
Exemple #3
0
    def _start_analog_output(self):
        """ Creates for each physical channel a task and its virtual channel

        @returns: error code: ok = 0, error = -1
        """
        try:
            # create a dictionary with physical channel name as key and a pointer as value {'/Dev1/AO0': c_void_p(None), ... }
            taskhandles = dict([(name, daq.TaskHandle(0))
                                for name in self._ao_channels])

            # if an analog task is already running, stop it first (safety if one of the created pointers already points somewhere)
            for channel in self._ao_channels:
                if taskhandles[channel].value is not None:
                    # stop analog output task
                    daq.DAQmxStopTask(taskhandles[channel])
                    # delete the configuration of the analog task
                    daq.DAQmxClearTask(taskhandles[channel])
                    # set the task handle to None as a safety
                    taskhandles[channel].value = None

            # create an individual task and a channel per analog output
            for n, channel in enumerate(self._ao_channels):
                daq.DAQmxCreateTask('', daq.byref(taskhandles[channel]))
                daq.DAQmxCreateAOVoltageChan(taskhandles[channel], channel, '',
                                             self._ao_voltage_ranges[n][0],
                                             self._ao_voltage_ranges[n][1],
                                             daq.DAQmx_Val_Volts, None)
            self.ao_taskhandles = taskhandles
        except:
            self.log.exception('Error starting analog output task.')
            return -1
        return 0
Exemple #4
0
def setup_hw_ao(fs, lines, expected_range, callback, callback_samples):
    # TODO: DAQmxSetAOTermCfg
    task = create_task()
    lb, ub = expected_range
    mx.DAQmxCreateAOVoltageChan(task, lines, '', lb, ub, mx.DAQmx_Val_Volts, '')
    mx.DAQmxCfgSampClkTiming(task, '', fs, mx.DAQmx_Val_Rising,
                             mx.DAQmx_Val_ContSamps, int(fs))

    # This controls how quickly we can update the buffer on the device. On some
    # devices it is not user-settable. On the X-series PCIe-6321 I am able to
    # change it. On the M-xeries PCI 6259 it appears to be fixed at 8191
    # samples.
    mx.DAQmxSetBufOutputOnbrdBufSize(task, 8191)

    # If the write reaches the end of the buffer and no new data has been
    # provided, do not loop around to the beginning and start over.
    mx.DAQmxSetWriteRegenMode(task, mx.DAQmx_Val_DoNotAllowRegen)

    mx.DAQmxSetBufOutputBufSize(task, int(callback_samples*100))

    result = ctypes.c_uint32()
    mx.DAQmxGetTaskNumChans(task, result)
    n_channels = result.value

    callback_helper = SamplesGeneratedCallbackHelper(callback, n_channels)
    cb_ptr = mx.DAQmxEveryNSamplesEventCallbackPtr(callback_helper)
    mx.DAQmxRegisterEveryNSamplesEvent(task,
                                       mx.DAQmx_Val_Transferred_From_Buffer,
                                       int(callback_samples), 0, cb_ptr, None)
    task._cb_ptr = cb_ptr
    return task
def makeAnalogOutSource(portString, handle, source, freq, amp, offset,
                        waveform):
    outScan = handle
    taskName = ''  # Name of the task (I don't know when this would not be an empty string...)
    input1Pointer = ctypes.byref(
        outScan
    )  # Equivalent to &setStates in C, the pointer to the task handle
    pydaqmx.DAQmxCreateTask(taskName, input1Pointer)

    chan = portString  # Location of the channel (this should be a physical channel, but it will be used as a virtual channel?)
    chanName = ""  # Name(s) to assign to the created virtual channel(s). "" means physical channel name will be used

    minVal = pydaqmx.float64(-10.0)
    maxVal = pydaqmx.float64(10.0)
    units = pydaqmx.DAQmx_Val_Volts
    pydaqmx.DAQmxCreateAOVoltageChan(outScan, chan, chanName, minVal, maxVal,
                                     units, 0)

    fSamp = 1000
    nSamp = 1000
    #source = None  # If you use an external clock, specify here, otherwise it should be None
    rate = pydaqmx.float64(
        fSamp
    )  # The sampling rate in samples per second per channel. If you use an external source for the Sample Clock, set this value to the maximum expected rate of that clock.
    edge = pydaqmx.DAQmx_Val_Rising  # Which edge of the clock (Rising/Falling) to acquire data
    sampMode = pydaqmx.DAQmx_Val_ContSamps  # Acquire samples continuously or just a finite number of samples
    sampPerChan = pydaqmx.uInt64(
        nSamp)  # Total number of sample to acquire for each channel
    pydaqmx.DAQmxCfgSampClkTiming(outScan, source, rate, edge, sampMode,
                                  sampPerChan)

    # writeArray = np.zeros((int(nSamp),), dtype=np.float64)
    if waveform == 'sin':
        x = 2 * np.pi * freq * np.array(range(nSamp)) / 1000.0
        writeArray = np.array(amp * np.sin(x) + offset, dtype=np.float64)
    if waveform == 'saw':
        # The amplitude is the peak-to-peak voltage in this waveform
        if freq != np.ceil(freq):
            print(
                "I don't understand decimals yet, the frequency I'm actually using is "
                + str(np.ceil(freq) + "Hz"))
        writeArray = amp / 1000.0 * (np.array(range(1000)) * freq %
                                     1000) + offset

    written = pydaqmx.int32()
    nSampPerChan = pydaqmx.int32(nSamp)
    pydaqmx.DAQmxWriteAnalogF64(outScan, nSampPerChan, pydaqmx.bool32(0),
                                pydaqmx.DAQmx_Val_WaitInfinitely,
                                pydaqmx.DAQmx_Val_GroupByChannel, writeArray,
                                ctypes.byref(written), None)
def setVoltage_2(val):
    try:
        taskHandle = tp.TaskHandle()
        daq.DAQmxCreateTask("",taskHandle)
#        print "taskHandle Value", taskHandle.value
        daq.DAQmxCreateAOVoltageChan(taskHandle,"Dev1/ao1","",0.0,10.0,cnst.DAQmx_Val_Volts,"")
        daq.DAQmxStartTask(taskHandle)
        daq.DAQmxWriteAnalogScalarF64(taskHandle,1,5.0,tp.float64(val),None)
        if not taskHandle == 0 :
#            print "Stopping Tasks\n"
            daq.DAQmxStopTask(taskHandle)
            daq.DAQmxClearTask(taskHandle)
        return 0
    except:
        errBuff=tp.create_string_buffer(b"",2048)
        daq.DAQmxGetExtendedErrorInfo(errBuff,2048)
        print(errBuff.value)
Exemple #7
0
    def _start_analog_output(self):
        try:
            if self._laser_ao_task is not None:
                daq.DAQmxStopTask(self._laser_ao_task)

                daq.DAQmxClearTask(self._laser_ao_task)

                self._laser_ao_task = None

            self._laser_ao_task = daq.TaskHandle()

            daq.DAQmxCreateTask('usbAO', daq.byref(self._laser_ao_task))
            daq.DAQmxCreateAOVoltageChan(self._laser_ao_task,
                                         self._NI_analog_channel,
                                         'Laser ao channel',
                                         self._NI_voltage_range[0],
                                         self._NI_voltage_range[1],
                                         daq.DAQmx_Val_Volts, '')
        except:
            self.log.exception('Error starting analog output task.')
            return -1
        return 0
Exemple #8
0
def channel_info(channels, channel_type):
    task = create_task()
    if channel_type in ('di', 'do', 'digital'):
        mx.DAQmxCreateDIChan(task, channels, '', mx.DAQmx_Val_ChanPerLine)
    elif channel_type == 'ao':
        mx.DAQmxCreateAOVoltageChan(task, channels, '', -10, 10,
                                    mx.DAQmx_Val_Volts, '')
    elif channel_type == 'ai':
        mx.DAQmxCreateAIVoltageChan(task, channels, '',
                                    mx.DAQmx_Val_Cfg_Default, -10, 10,
                                    mx.DAQmx_Val_Volts, '')

    channels = ctypes.create_string_buffer('', 4096)
    mx.DAQmxGetTaskChannels(task, channels, len(channels))
    devices = ctypes.create_string_buffer('', 4096)
    mx.DAQmxGetTaskDevices(task, devices, len(devices))
    mx.DAQmxClearTask(task)

    return {
        'channels': [c.strip() for c in channels.value.split(',')],
        'devices': [d.strip() for d in devices.value.split(',')],
    }
Exemple #9
0
    def start(self, test=False, **kwargs):
        """
        1. Creates a task using settings.
        2. Starts the task.

        You need to call wait_and_clean() after you start()

        kwargs are sent to self() to set parameters.
        """

        self(**kwargs)

        # make sure everything that should be a list is a list
        if not isinstance(self["ao_channels"], Iterable):
            self["ao_channels"] = [self["ao_channels"]]

        # if the first element of the waveform is not an array
        if len(_n.shape(self["ao_waveforms"][0])) < 1:
            self["ao_waveforms"] = [self["ao_waveforms"]]

        # create the task object. This doesn't return an object, because
        # National Instruments. Instead, we have this handle, and we need
        # to be careful about clearing the thing attached to the handle.
        debug("output task handle")
        _mx.DAQmxClearTask(self._handle)
        _mx.DAQmxCreateTask(self["ao_task_name"], _mx.byref(self._handle))

        # create all the output channels
        debug("output channels")

        # this is an array of output data arrays, grouped by channel
        samples = 0
        data = _n.array([])

        # loop over all the channels
        for n in range(len(self["ao_channels"])):

            # get the channel-specific attributes
            name = self["ao_channels"][n]
            nickname = name.replace("/", "")

            debug(name)

            if isinstance(self["ao_min"], Iterable): ao_min = self["ao_min"][n]
            else: ao_min = self["ao_min"]

            if isinstance(self["ao_max"], Iterable): ao_max = self["ao_max"][n]
            else: ao_max = self["ao_max"]

            if isinstance(self["ao_units"], Iterable):
                ao_units = self["ao_units"][n]
            else:
                ao_units = self["ao_units"]

            waveform = self["ao_waveforms"][n]

            # add an output channel
            _mx.DAQmxCreateAOVoltageChan(self._handle, name, nickname, ao_min,
                                         ao_max, ao_units, "")

            # add the corresponding output wave to the master data array
            debug("data", data, "waveform", waveform)
            data = _n.concatenate([data, waveform])

            # Set the samples number to the biggest output array size
            samples = max(len(self["ao_waveforms"][n]), samples)

        # Configure the clock
        debug("output clock")

        # make sure we don't exceed the max
        #ao_max_rate = _mx.float64()
        #_mx.DAQmxGetSampClkMaxRate(self._handle, _mx.byref(ao_max_rate))
        #if self['ao_rate'] > ao_max_rate.value:
        #    print "ERROR: ao_rate is too high! Current max = "+str(ao_max_rate.value)
        #    self.clean()
        #    return False

        _mx.DAQmxCfgSampClkTiming(self._handle, self["ao_clock_source"],
                                  self["ao_rate"], self["ao_clock_edge"],
                                  self["ao_mode"], samples)

        # if we're supposed to, export a signal
        if not self['ao_export_terminal'] == None:
            _mx.DAQmxExportSignal(self._handle, self['ao_export_signal'],
                                  self['ao_export_terminal'])

        # update to the actual ao_rate (may be different than what was set)
        ao_rate = _mx.float64()
        _mx.DAQmxGetSampClkRate(self._handle, _mx.byref(ao_rate))
        debug("output actual ao_rate =", ao_rate.value)
        self(ao_rate=ao_rate.value)

        # Configure the trigger
        debug("output trigger")
        _mx.DAQmxCfgDigEdgeStartTrig(self._handle, self["ao_trigger_source"],
                                     self["ao_trigger_slope"])

        # Set the post-trigger delay
        try:
            n = self["ao_delay"] * 10e6
            if n < 2: n = 2
            _mx.DAQmxSetStartTrigDelayUnits(self._handle, _mx.DAQmx_Val_Ticks)
            _mx.DAQmxSetStartTrigDelay(self._handle, n)
        except:
            _traceback.print_exc()

        # write the data to the analog outputs (arm it)
        debug("output write", len(data))
        write_success = _mx.int32()
        _mx.DAQmxWriteAnalogF64(
            self._handle,
            samples,
            False,
            self["ao_timeout"],
            _mx.
            DAQmx_Val_GroupByChannel,  # Type of grouping of data in the array (for interleaved use DAQmx_Val_GroupByScanNumber)
            data,  # Array of data to output
            _mx.byref(write_success),  # Output the number of successful write
            None)  # Reserved input (just put in None...)
        debug("success:", samples, write_success)

        if test:
            self.clean()
        else:
            # Start the task!!
            debug("output start")
            try:
                _mx.DAQmxStartTask(self._handle)
            except:
                _traceback.print_exc()

        return True
Exemple #10
0
def setup_hw_ao(channels, buffer_duration, callback_interval, callback,
                task_name='hw_ao'):

    lines = get_channel_property(channels, 'channel', True)
    names = get_channel_property(channels, 'name', True)
    expected_ranges = get_channel_property(channels, 'expected_range', True)
    start_trigger = get_channel_property(channels, 'start_trigger')
    terminal_mode = get_channel_property(channels, 'terminal_mode')
    terminal_mode = NIDAQEngine.terminal_mode_map[terminal_mode]
    task = create_task(task_name)
    merged_lines = ','.join(lines)

    for line, name, (vmin, vmax) in zip(lines, names, expected_ranges):
        log.debug(f'Configuring line %s (%s) with voltage range %f-%f', line, name, vmin, vmax)
        mx.DAQmxCreateAOVoltageChan(task, line, name, vmin, vmax, mx.DAQmx_Val_Volts, '')

    properties = setup_timing(task, channels)

    result = ctypes.c_double()
    try:
        for line in lines:
            mx.DAQmxGetAOGain(task, line, result)
            properties['{} AO gain'.format(line)] = result.value
    except:
        # This means that the gain is not settable
        properties['{} AO gain'.format(line)] = 0

    fs = properties['sample clock rate']
    log_ao.info('AO properties: %r', properties)

    if terminal_mode is not None:
        mx.DAQmxSetAOTermCfg(task, merged_lines, terminal_mode)

    # If the write reaches the end of the buffer and no new data has been
    # provided, do not loop around to the beginning and start over.
    mx.DAQmxSetWriteRegenMode(task, mx.DAQmx_Val_DoNotAllowRegen)
    mx.DAQmxSetWriteRelativeTo(task, mx.DAQmx_Val_CurrWritePos)

    callback_samples = int(round(fs*callback_interval))

    if buffer_duration is None:
        log_ao.debug('Buffer duration not provided. Setting to 10x callback.')
        buffer_samples = round(callback_samples*10)
    else:
        buffer_samples = round(buffer_duration*fs)

    # Now, make sure that buffer_samples is an integer multiple of callback_samples.
    log_ao.debug('Requested buffer samples %d', buffer_samples)
    n = int(round(buffer_samples / callback_samples))
    buffer_samples = callback_samples * n
    log_ao.debug('Coerced buffer samples to %d (%dx %d callback_samples)',
                 buffer_samples, n, callback_samples)

    log_ao.debug('Setting output buffer size to %d samples', buffer_samples)
    mx.DAQmxSetBufOutputBufSize(task, buffer_samples)
    task._buffer_samples = buffer_samples

    result = ctypes.c_uint32()
    mx.DAQmxGetTaskNumChans(task, result)
    task._n_channels = result.value
    log_ao.debug('%d channels in task', task._n_channels)

    #mx.DAQmxSetAOMemMapEnable(task, lines, True)
    #mx.DAQmxSetAODataXferReqCond(task, lines, mx.DAQmx_Val_OnBrdMemHalfFullOrLess)

    # This controls how quickly we can update the buffer on the device. On some
    # devices it is not user-settable. On the X-series PCIe-6321 I am able to
    # change it. On the M-xeries PCI 6259 it appears to be fixed at 8191
    # samples. Haven't really been able to do much about this.
    mx.DAQmxGetBufOutputOnbrdBufSize(task, result)
    task._onboard_buffer_size = result.value
    log_ao.debug('Onboard buffer size %d', task._onboard_buffer_size)

    result = ctypes.c_int32()
    mx.DAQmxGetAODataXferMech(task, merged_lines, result)
    log_ao.debug('Data transfer mechanism %d', result.value)
    mx.DAQmxGetAODataXferReqCond(task, merged_lines, result)
    log_ao.debug('Data transfer condition %d', result.value)
    #result = ctypes.c_uint32()
    #mx.DAQmxGetAOUseOnlyOnBrdMem(task, merged_lines, result)
    #log_ao.debug('Use only onboard memory %d', result.value)
    #mx.DAQmxGetAOMemMapEnable(task, merged_lines, result)
    #log_ao.debug('Memory mapping enabled %d', result.value)

    #mx.DAQmxGetAIFilterDelayUnits(task, merged_lines, result)
    #log_ao.debug('AI filter delay unit %d', result.value)

    #result = ctypes.c_int32()
    #mx.DAQmxGetAODataXferMech(task, result)
    #log_ao.debug('DMA transfer mechanism %d', result.value)

    log_ao.debug('Creating callback after every %d samples', callback_samples)
    task._cb = partial(hw_ao_helper, callback)
    task._cb_ptr = mx.DAQmxEveryNSamplesEventCallbackPtr(task._cb)
    mx.DAQmxRegisterEveryNSamplesEvent(
        task, mx.DAQmx_Val_Transferred_From_Buffer, int(callback_samples), 0,
        task._cb_ptr, None)

    mx.DAQmxTaskControl(task, mx.DAQmx_Val_Task_Reserve)
    task._names = verify_channel_names(task, names)
    task._devices = device_list(task)
    task._fs = fs
    return task
import numpy as np
import time

outScan = pydaqmx.TaskHandle()
taskName = ''  # Name of the task (I don't know when this would not be an empty string...)
input1Pointer = ctypes.byref(
    outScan)  # Equivalent to &setStates in C, the pointer to the task handle
pydaqmx.DAQmxCreateTask(taskName, input1Pointer)

chan = 'Dev1/ao1'  # Location of the channel (this should be a physical channel, but it will be used as a virtual channel?)
chanName = ""  # Name(s) to assign to the created virtual channel(s). "" means physical channel name will be used

minVal = pydaqmx.float64(-10.0)
maxVal = pydaqmx.float64(10.0)
units = pydaqmx.DAQmx_Val_Volts
pydaqmx.DAQmxCreateAOVoltageChan(outScan, chan, chanName, minVal, maxVal,
                                 units, 0)

fSamp = 1000
nSamp = 1000
freq = 50  # Frequency in Hertz
amp = 10  # Amplitude in volts
source = None  # If you use an external clock, specify here, otherwise it should be None
rate = pydaqmx.float64(
    fSamp
)  # The sampling rate in samples per second per channel. If you use an external source for the Sample Clock, set this value to the maximum expected rate of that clock.
edge = pydaqmx.DAQmx_Val_Rising  # Which edge of the clock (Rising/Falling) to acquire data
sampMode = pydaqmx.DAQmx_Val_ContSamps  # Acquire samples continuously or just a finite number of samples
sampPerChan = pydaqmx.uInt64(
    nSamp)  # Total number of sample to acquire for each channel
pydaqmx.DAQmxCfgSampClkTiming(outScan, source, rate, edge, sampMode,
                              sampPerChan)
Exemple #12
0
 def activate_AO(self, taskhandles, channel, voltage_range):
     daq.DAQmxCreateTask('', daq.byref(taskhandles))
     daq.DAQmxCreateAOVoltageChan(taskhandles, channel, '',
                                  voltage_range[0], voltage_range[1],
                                  daq.DAQmx_Val_Volts, None)
     return taskhandles
Exemple #13
0
    def set_up_scanner(self, counter_channel = None, photon_source = None, clock_channel = None, scanner_ao_channels = None):
        """ Configures the actual scanner with a given clock. 
        
        @param string counter_channel: if defined, this is the physical channel of the counter
        @param string photon_source: if defined, this is the physical channel where the photons are to count from
        @param string clock_channel: if defined, this specifies the clock for the counter
        @param string scanner_ao_channels: if defined, this specifies the analoque output channels
        
        @return int: error code (0:OK, -1:error)
        """
        
        if self._scanner_clock_daq_task == None and clock_channel == None:            
            self.logMsg('No clock running, call set_up_clock before starting the counter.', \
            msgType='error')
            return -1
        if self._scanner_counter_daq_task != None:            
            self.logMsg('Another counter is already running, close this one first.', \
            msgType='error')
            return -1
            
        if counter_channel != None:
            self._scanner_counter_channel = counter_channel
        if photon_source != None:
            self._photon_source = photon_source
        
        if clock_channel != None:
            self._my_scanner_clock_channel = clock_channel
        else: 
            self._my_scanner_clock_channel = self._scanner_clock_channel
            
        if scanner_ao_channels != None:
            self._scanner_ao_channels = scanner_ao_channels
        
        # init ao channels / task for scanner, should always be active
        # the type definition for the task, an unsigned integer datatype (uInt32):
        self._scanner_ao_task = daq.TaskHandle()
        
        daq.DAQmxCreateTask('', daq.byref(self._scanner_ao_task))
        
        # Assign the created task to an analog output voltage channel
        daq.DAQmxCreateAOVoltageChan(self._scanner_ao_task,           # add to this task
                                     self._scanner_ao_channels, 
                                     '',  # use sanncer ao_channels, name = ''
                                     self._voltage_range[0],          # min voltage
                                     self._voltage_range[1],           # max voltage
                                     daq.DAQmx_Val_Volts,'')       # units is Volt
        
        # set task timing to on demand, i.e. when demanded by software
        daq.DAQmxSetSampTimingType(self._scanner_ao_task, daq.DAQmx_Val_OnDemand)
        #self.set_scanner_command_length(self._DefaultAOLength)
        
        self._scanner_counter_daq_task = daq.TaskHandle()
        daq.DAQmxCreateTask('', daq.byref(self._scanner_counter_daq_task))
        
        # TODO: change this to DAQmxCreateCISemiPeriodChan
        daq.DAQmxCreateCIPulseWidthChan(self._scanner_counter_daq_task,    #add to this task
                                        self._scanner_counter_channel,  #use this counter
                                        '',  #name
                                        0,  #expected minimum value
                                        self._max_counts*self._scanner_clock_frequency,    #expected maximum value
                                        daq.DAQmx_Val_Ticks,     #units of width measurement, here photon ticks
                                        daq.DAQmx_Val_Rising, '') #start pulse width measurement on rising edge

        #set the pulses to counter self._trace_counter_in
        daq.DAQmxSetCIPulseWidthTerm(self._scanner_counter_daq_task, 
                                     self._scanner_counter_channel, 
                                     self._my_scanner_clock_channel+'InternalOutput')
                                     
        #set the timebase for width measurement as self._photon_source
        daq.DAQmxSetCICtrTimebaseSrc(self._scanner_counter_daq_task, 
                                     self._scanner_counter_channel, 
                                     self._photon_source )
        
        return 0
Exemple #14
0
def setup_sw_ao(lines, expected_range):
    # TODO: DAQmxSetAOTermCfg
    task = create_task()
    lb, ub = expected_range
    mx.DAQmxCreateAOVoltageChan(task, lines, '', lb, ub, mx.DAQmx_Val_Volts, '')
    return task