def DAQ_function():
    # Query the Arduino for its state
    success, tmp_state = ard.query_ascii_values("?", delimiter="\t")
    if not (success):
        dprint("'%s' reports IOError" % ard.name)
        return False

    # Parse readings into separate state variables
    try:
        state.time, state.reading_1 = tmp_state
        state.time /= 1000
    except Exception as err:
        pft(err, 3)
        dprint("'%s' reports IOError" % ard.name)
        return False

    # Use Arduino time or PC time?
    USE_PC_TIME = True
    now = time.perf_counter() if USE_PC_TIME else state.time
    if qdev_ard.update_counter_DAQ == 1:
        state.time_0 = now
        state.time = 0
    else:
        state.time = now - state.time_0

    # For demo purposes: Quit automatically after N updates
    if qdev_ard.update_counter_DAQ > 1000:
        app.quit()

    return True
    def DAQ_function(self):
        DEBUG_local = False
        if DEBUG_local:
            tick = time.perf_counter()

        if not self.dev.query_V_meas():
            return False
        if not self.dev.query_I_meas():
            return False
        if not self.dev.query_LSR():
            return False
        if not self.dev.query_ENA_output():
            return False

        # Explicitly force the output state to off when the output got disabled
        # on a hardware level by a triggered protection or fault.
        if self.dev.state.ENA_output and self.dev.state.LSR_is_tripped:
            self.dev.state.ENA_output = False
            self.dev.set_ENA_output(False)

        if DEBUG_local:
            tock = time.perf_counter()
            dprint("%s: done in %i" % (self.dev.name, tock - tick))

        return True
def DAQ_function():
    # Date-time keeping
    str_cur_date, str_cur_time, str_cur_datetime = get_current_date_time()

    # Query the Arduino for its state
    success, tmp_state = ard.query_ascii_values("?", delimiter="\t")
    if not (success):
        dprint("'%s' reports IOError @ %s %s" %
               (ard.name, str_cur_date, str_cur_time))
        return False

    # Parse readings into separate state variables
    try:
        state.time, state.reading_1 = tmp_state
        state.time /= 1000
    except Exception as err:
        pft(err, 3)
        dprint("'%s' reports IOError @ %s %s" %
               (ard.name, str_cur_date, str_cur_time))
        return False

    if USE_PC_TIME:
        state.time = time.perf_counter()

    # Add readings to chart history
    window.history_chart_curve.appendData(state.time, state.reading_1)

    # Logging to file
    log.update(filepath=str_cur_datetime + ".txt")

    # Return success
    return True
def test_Worker_jobs__start_without_create():
    print_title("Worker_jobs - start without create")
    import pytest

    qdev = QDeviceIO(FakeDevice())

    with pytest.raises(SystemExit) as pytest_wrapped_e:
        qdev.start_worker_jobs()
    assert pytest_wrapped_e.type == SystemExit
    dprint("Exit code: %i" % pytest_wrapped_e.value.code)
    assert pytest_wrapped_e.value.code == 404
def test_Worker_jobs__no_device_attached():
    print_title("Worker_jobs - no device attached")
    import pytest

    qdev = QDeviceIO()

    with pytest.raises(SystemExit) as pytest_wrapped_e:
        qdev.create_worker_jobs()
    assert pytest_wrapped_e.type == SystemExit
    dprint("Exit code: %i" % pytest_wrapped_e.value.code)
    assert pytest_wrapped_e.value.code == 99
def test_attach_device_twice():
    print_title("Attach device twice")
    import pytest

    qdev = QDeviceIO(FakeDevice())

    with pytest.raises(SystemExit) as pytest_wrapped_e:
        qdev.attach_device(FakeDevice())
    assert pytest_wrapped_e.type == SystemExit
    dprint("Exit code: %i" % pytest_wrapped_e.value.code)
    assert pytest_wrapped_e.value.code == 22
def DAQ_function():
    # Date-time keeping
    str_cur_date, str_cur_time, str_cur_datetime = get_current_date_time()

    # Query the Arduino for its state
    success, tmp_state = ard.query_ascii_values("?", delimiter="\t")
    if not (success):
        dprint(
            "'%s' reports IOError @ %s %s"
            % (ard.name, str_cur_date, str_cur_time)
        )
        return False

    # Parse readings into separate state variables
    try:
        (
            state.time,
            state.ds_temp,
            state.bme_temp,
            state.bme_humi,
            state.bme_pres,
        ) = tmp_state
        state.time /= 1000  # Arduino time, [msec] to [s]
        state.bme_pres /= 100  # [Pa] to [mbar]
    except Exception as err:
        pft(err, 3)
        dprint(
            "'%s' reports IOError @ %s %s"
            % (ard.name, str_cur_date, str_cur_time)
        )
        return False

    # Catch very intermittent DS18B20 sensor errors
    if state.ds_temp <= -127.0:
        state.ds_temp = np.nan

    # We will use PC time instead
    state.time = time.perf_counter()

    # Add readings to chart histories
    window.tscurve_julabo_setp.appendData(state.time, julabo.state.setpoint)
    window.tscurve_julabo_bath.appendData(state.time, julabo.state.bath_temp)
    window.tscurve_ds_temp.appendData(state.time, state.ds_temp)
    window.tscurve_bme_temp.appendData(state.time, state.bme_temp)
    window.tscurve_bme_humi.appendData(state.time, state.bme_humi)
    window.tscurve_bme_pres.appendData(state.time, state.bme_pres)

    # Logging to file
    log.update(filepath=str_cur_datetime + ".txt", mode="w")

    # Return success
    return True
def DAQ_function():
    # Date-time keeping
    str_cur_date, str_cur_time, str_cur_datetime = get_current_date_time()

    state.update_counter_DAQ += 1

    # Keep track of the obtained DAQ rate
    if not state.QET_rate.isValid():
        state.QET_rate.start()
    else:
        # Obtained DAQ rate
        state.rate_accumulator += 1
        dT = state.QET_rate.elapsed()

        if dT >= 1000:  # Evaluate every N elapsed milliseconds. Hard-coded.
            state.QET_rate.restart()
            try:
                state.obtained_DAQ_rate_Hz = state.rate_accumulator / dT * 1e3
            except ZeroDivisionError:
                state.obtained_DAQ_rate_Hz = np.nan

            state.rate_accumulator = 0

    # Query the Arduino for its state
    success, tmp_state = ard.query_ascii_values("?", delimiter="\t")
    if not (success):
        dprint("'%s' reports IOError @ %s %s" %
               (ard.name, str_cur_date, str_cur_time))
        sys.exit(0)

    # Parse readings into separate state variables
    try:
        state.time, state.reading_1 = tmp_state
        state.time /= 1000
    except Exception as err:
        pft(err, 3)
        dprint("'%s' reports IOError @ %s %s" %
               (ard.name, str_cur_date, str_cur_time))
        sys.exit(0)

    if USE_PC_TIME:
        state.time = time.perf_counter()

    # Add readings to chart histories
    window.history_chart_curve.appendData(state.time, state.reading_1)

    # Logging to file
    log.update(filepath=str_cur_datetime + ".txt")

    # We update the GUI right now because this is a singlethread demo
    window.update_GUI()
def DAQ_function():
    # Date-time keeping
    str_cur_date, str_cur_time, str_cur_datetime = get_current_date_time()

    # Query the Arduino for its state
    success, tmp_state = ard.query_ascii_values("?", delimiter="\t")
    if not (success):
        dprint("'%s' reports IOError @ %s %s" %
               (ard.name, str_cur_date, str_cur_time))
        return False

    # Parse readings into separate state variables
    try:
        (
            state.time,
            state.dht22_temp,
            state.dht22_humi,
            ds18b20_temp,
        ) = tmp_state
        state.time /= 1000  # Arduino time, [msec] to [s]
    except Exception as err:
        pft(err, 3)
        dprint("'%s' reports IOError @ %s %s" %
               (ard.name, str_cur_date, str_cur_time))
        return False

    # Optional extra sensor to register the heater surface temperature
    # print("%.2f" % ds18b20_temp)

    # We will use PC time instead
    state.time = time.perf_counter()

    # PID control
    pid.set_mode(
        mode=(psu.state.ENA_output and state.pid_enabled
              and not np.isnan(state.dht22_temp)),
        current_input=state.dht22_temp,
        current_output=psu.state.V_source,
    )

    if pid.compute(current_input=state.dht22_temp):
        # New PID output got computed -> send new voltage to PSU
        qdev_psu.send(qdev_psu.dev.set_V_source, pid.output)

        # Print debug info to the terminal
        dprint("Tp=%7.3f   Ti=%7.3f   outp=%7.3f" %
               (pid.pTerm, pid.iTerm, pid.output))

    # Add readings to chart histories
    window.tscurve_dht22_temp.appendData(state.time, state.dht22_temp)
    window.tscurve_dht22_humi.appendData(state.time, state.dht22_humi)
    window.tscurve_power.appendData(state.time, psu.state.P_meas)

    # Logging to file
    log.update(filepath=str_cur_datetime + ".txt", mode="w")

    # Return success
    return True
Example #10
0
    def initialize(self, current_input, current_output):
        """Does all the things that need to happen to ensure a bumpless
        transfer from manual to automatic mode.
        """
        self.iTerm = current_output
        self.last_input = current_input

        if self.debug:
            dprint("PID init")
            if self.iTerm < self.output_limit_min:
                dprint("@PID init: iTerm < output_limit_min: integral windup")
            elif self.iTerm > self.output_limit_max:
                dprint("@PID init: iTerm > output_limit_max: integral windup")

        self.iTerm = np.clip(self.iTerm, self.output_limit_min,
                             self.output_limit_max)
    def scan_ports(self, verbose: bool = True) -> bool:
        """Scan over all serial ports and try to establish a connection. See
        further the description at :meth:`connect_at_port`.

        Args:
            verbose (:obj:`bool`, optional):
                Print a `"Scanning ports for: "`-message to the terminal?

                Default: :const:`True`

        Returns:
            True if successful, False otherwise.
        """

        if verbose:
            _print_hrule(True)
            if (self._ID_validation_query is None
                    or self._valid_ID_specific is None):
                dprint("  Scanning ports for: %s" % self.long_name,
                       ANSI.YELLOW)
            else:
                dprint(
                    "  Scanning ports for: %s `%s`" %
                    (self.long_name, self._valid_ID_specific),
                    ANSI.YELLOW,
                )
            _print_hrule()

        # Ports is a list of tuples
        ports = list(serial.tools.list_ports.comports())
        for p in ports:
            port = p[0]
            if self.connect_at_port(port, verbose=False):
                return True
            else:
                continue

        # Scanned over all the ports without finding a match
        dprint("  Error: Device not found.\n", ANSI.RED)
        return False
def test_dprint_in_red():
    with mock.patch("sys.stdout", new=io.StringIO()) as fake_stdout:
        dprint("In red", ANSI.RED)

    assert fake_stdout.getvalue() == "\x1b[1;31mIn red\x1b[1;37m\n"
Example #13
0
def trigger_update_psus():
    if DEBUG:
        dprint("timer_psus: wake up all DAQ")
    for psu_qdev in psus_qdev:
        psu_qdev.worker_DAQ.wake_up()
Example #14
0
    def DAQ_function(self):
        tick = get_tick()

        # Clear input and output buffers of the device. Seems to resolve
        # intermittent communication time-outs.
        self.dev.device.clear()

        success = True
        if self.is_MUX_scanning:
            success &= self.dev.init_scan()  # Init scan

            if success:
                self.dev.wait_for_OPC()  # Wait for OPC
                if self.worker_DAQ.debug:
                    tock = get_tick()
                    dprint("opc? in: %i" % (tock - tick))
                    tick = tock

                success &= self.dev.fetch_scan()  # Fetch scan
                if self.worker_DAQ.debug:
                    tock = get_tick()
                    dprint("fetc in: %i" % (tock - tick))
                    tick = tock

            if success:
                self.dev.wait_for_OPC()  # Wait for OPC
                if self.worker_DAQ.debug:
                    tock = get_tick()
                    dprint("opc? in: %i" % (tock - tick))
                    tick = tock

        if success:
            # Do not throw additional timeout exceptions when .init_scan()
            # might have already failed. Hence this check for no success.
            self.dev.query_all_errors_in_queue()  # Query errors
            if self.worker_DAQ.debug:
                tock = get_tick()
                dprint("err? in: %i" % (tock - tick))
                tick = tock

            # The next statement seems to trigger timeout, but very
            # intermittently (~once per 20 minutes). After this timeout,
            # everything times out.
            # self.dev.wait_for_OPC()
            # if self.worker_DAQ.debug:
            #    tock = get_tick()
            #    dprint("opc? in: %i" % (tock - tick))
            #    tick = tock

            # NOTE: Another work-around to intermittent time-outs might
            # be sending self.dev.clear() every iter to clear the input and
            # output buffers. This is now done at the start of this function.

        # Optional user-supplied function to run. You can use this function to,
        # e.g., parse out the scan readings into separate variables and
        # post-process this data or log it.
        if self.DAQ_postprocess_MUX_scan_function is not None:
            self.DAQ_postprocess_MUX_scan_function()

        if self.worker_DAQ.debug:
            tock = get_tick()
            dprint("extf in: %i" % (tock - tick))
            tick = tock

        return success
 def DAQ_function():
     # Must return True when successful, False otherwise
     reply = dev.fake_query_1()
     dprint(" " * 50 + "%.1f Hz" % qdev.obtained_DAQ_rate_Hz)
     return reply[-4:] == "0101"
def _print_hrule(leading_newline=False):
    dprint(("\n" if leading_newline else "") + "-" * 60, ANSI.YELLOW)
 def print_success(success_str: str):
     dprint(success_str, ANSI.GREEN)
     dprint(" " * 16 + "--> %s\n" % self.name, ANSI.GREEN)
    def connect_at_port(self, port: str, verbose: bool = True) -> bool:
        """Open the serial port at address ``port`` and try to establish a
        connection.

        If the connection is successful and :meth:`set_ID_validation_query`
        was not set, then this function will return :const:`True`.

        If :meth:`set_ID_validation_query` was set, then this function will
        return :const:`True` or :const:`False` depending on the validation
        scheme as explained in :meth:`set_ID_validation_query`.

        Args:
            port (:obj:`str`):
                Serial port address to open.

            verbose (:obj:`bool`, optional):
                Print a `"Connecting to: "`-message to the terminal?

                Default: :const:`True`

        Returns:
            True if successful, False otherwise.
        """
        def print_success(success_str: str):
            dprint(success_str, ANSI.GREEN)
            dprint(" " * 16 + "--> %s\n" % self.name, ANSI.GREEN)

        if verbose:
            _print_hrule(True)
            if (self._ID_validation_query is None
                    or self._valid_ID_specific is None):
                dprint("  Connecting to: %s" % self.long_name, ANSI.YELLOW)
            else:
                dprint(
                    "  Connecting to: %s `%s`" %
                    (self.long_name, self._valid_ID_specific),
                    ANSI.YELLOW,
                )
            _print_hrule()

        print("  @ %-11s " % port, end="")
        try:
            # Open the serial port
            self.ser = serial.Serial(port=port, **self.serial_settings)
        except serial.SerialException:
            print("Could not open port.")
            return False  # --> leaving
        except Exception as err:
            pft(err, 3)
            sys.exit(0)  # --> leaving

        if self._ID_validation_query is None:
            # Found any device
            print_success("Any Success!")
            self.is_alive = True
            return True  # --> leaving

        # Optional validation query
        try:
            self._force_query_to_raise_on_timeout = True
            self.is_alive = True  # We must assume communication is possible
            reply_broad, reply_specific = self._ID_validation_query()
        except:
            print("Wrong or no device.")
            self.close(ignore_exceptions=True)
            return False  # --> leaving
        finally:
            self._force_query_to_raise_on_timeout = False

        if reply_broad == self._valid_ID_broad:
            if reply_specific is not None:
                print("Found `%s`: " % reply_specific, end="")

            if self._valid_ID_specific is None:
                # Found a matching device in a broad sense
                print_success("Broad Success!")
                self.is_alive = True
                return True  # --> leaving

            elif reply_specific == self._valid_ID_specific:
                # Found a matching device in a specific sense
                print_success("Specific Success!")
                self.is_alive = True
                return True  # --> leaving

        print("Wrong device.")
        self.close(ignore_exceptions=True)
        return False
def print_title(title):
    dprint("\n%s" % title, ANSI.PURPLE)
    dprint("-" * 50, ANSI.PURPLE)
Example #20
0
    def compute(self, current_input):
        """Compute new PID output. This function should be called repeatedly,
        preferably at a fixed time interval.
        Returns True when the output is computed, false when nothing has been
        done.
        """

        now = time.perf_counter()  # [s]
        time_step = now - self.last_time

        if (not self.in_auto) or np.isnan(self.setpoint):
            self.last_time = now
            return False

        _input = current_input
        error = self.setpoint - _input

        # Proportional term
        self.pTerm = self.kp * error

        # Integral term
        # self.iTerm = self.iTerm + (self.ki * error)
        self.iTerm = self.iTerm + (self.ki * time_step * error)

        if self.debug:
            if self.iTerm < self.output_limit_min:
                dprint("iTerm < output_limit_min: integral windup")
            elif self.iTerm > self.output_limit_max:
                dprint("iTerm > output_limit_max: integral windup")

        # Prevent integral windup
        self.iTerm = np.clip(self.iTerm, self.output_limit_min,
                             self.output_limit_max)

        # Derivative term
        # Prevent derivative kick: really good to do!
        # self.dTerm = -self.kd * (_input - self.last_input)
        self.dTerm = -self.kd / time_step * (_input - self.last_input)

        # Compute PID Output
        self.output = self.pTerm + self.iTerm + self.dTerm

        if self.debug:
            dprint("%i" % (time_step * 1000))
            dprint("%.1f %.1f %.1f" % (self.pTerm, self.iTerm, self.dTerm))
            dprint((" " * 14 + "%.2f") % self.output)

            if self.output < self.output_limit_min:
                dprint("output < output_limit_min: output clamped")
            elif self.output > self.output_limit_max:
                dprint("output > output_limit_max: output clamped")

        # Clamp the output to its limits
        self.output = np.clip(self.output, self.output_limit_min,
                              self.output_limit_max)

        # Remember some variables for next time
        self.last_input = _input
        self.last_time = now

        return True
    def DAQ_function(self):
        DEBUG_local = False
        if DEBUG_local:
            tick = get_tick()

        # Clear input and output buffers of the device. Seems to resolve
        # intermittent communication time-outs.
        self.dev.device.clear()

        # Finish all operations at the device first
        if not self.dev.wait_for_OPC():
            return False

        if not self.dev.query_V_meas():
            return False
        if not self.dev.query_I_meas():
            return False

        # --------------------
        #   Heater power PID
        # --------------------
        # PID controllers work best when the process and control variables have
        # a linear relationship.
        # Here:
        #   Process var: V (voltage)
        #   Control var: P (power)
        #   Relation   : P = R / V^2
        #
        # Hence, we transform P into P_star
        #   Control var: P_star = sqrt(P)
        #   Relation   : P_star = sqrt(R) / V
        # When we assume R remains constant (which is not the case as the
        # resistance is a function of the heater temperature, but the dependence
        # is expected to be insignificant in our small temperature range of 20
        # to 100 deg C), we now have linearized the PID feedback relation.
        self.dev.PID_power.set_mode(
            (self.dev.state.ENA_output and self.dev.state.ENA_PID),
            self.dev.state.P_meas,
            self.dev.state.V_source,
        )

        self.dev.PID_power.setpoint = np.sqrt(self.dev.state.P_source)
        if self.dev.PID_power.compute(np.sqrt(self.dev.state.P_meas)):
            # New PID output got computed -> send new voltage to PSU
            if self.dev.PID_power.output < 1:
                # PSU does not regulate well below 1 V, hence clamp to 0
                self.dev.PID_power.output = 0
            if not self.dev.set_V_source(self.dev.PID_power.output):
                return False
            # Wait for the set_V_source operation to finish.
            # Takes ~ 300 ms to complete with wait_for_OPC.
            if not self.dev.wait_for_OPC():
                return False

        if not self.dev.query_ENA_OCP():
            return False
        if not self.dev.query_status_OC():
            return False
        if not self.dev.query_status_QC():
            return False
        if not self.dev.query_ENA_output():
            return False

        # Explicitly force the output state to off when the output got disabled
        # on a hardware level by a triggered protection or fault.
        if self.dev.state.ENA_output & (
            self.dev.state.status_QC_OV
            | self.dev.state.status_QC_OC
            | self.dev.state.status_QC_PF
            | self.dev.state.status_QC_OT
            | self.dev.state.status_QC_INH
        ):
            self.dev.state.ENA_output = False
            self.dev.set_ENA_output(False)

        if DEBUG_local:
            tock = get_tick()
            dprint("%s: done in %i" % (self.dev.name, tock - tick))

        # Check if there are errors in the device queue and retrieve all
        # if any and append these to 'dev.state.all_errors'.
        if DEBUG_local:
            dprint("%s: query errors" % self.dev.name)
            tick = get_tick()
        self.dev.query_all_errors_in_queue()
        if DEBUG_local:
            tock = get_tick()
            dprint("%s: stb done in %i" % (self.dev.name, tock - tick))

        return True
def tprint_tab(str_msg, ANSI_color=None):
    dprint(" " * 60 + "%.4f %s" % (time.perf_counter(), str_msg), ANSI_color)
def test_dprint():
    with mock.patch("sys.stdout", new=io.StringIO()) as fake_stdout:
        dprint("No color")

    assert fake_stdout.getvalue() == "No color\n"
Example #24
0
    def listen_to_lockin_amp(
        self,
    ) -> Tuple[bool, Union[int, float], np.ndarray, np.ndarray, np.ndarray,
               np.ndarray]:
        """Reads incoming data packets coming from the lock-in amp. This method
        is blocking until it receives an EOM (end-of-message) sentinel or until
        it times out.

        Returns:
            Tuple (
                success: bool
                counter: int | numpy.nan
                time   : numpy.ndarray, units [us]
                ref_X  : numpy.ndarray, units [non-dim]
                ref_Y  : numpy.ndarray, units [non-dim]
                sig_I  : numpy.ndarray, units [V]
            )
        """
        failed = False, None, [np.nan], [np.nan], [np.nan], [np.nan]
        c = self.config  # Shorthand alias

        ans_bytes = self.read_until_EOM()
        # dprint("EOM found with %i bytes and..." % len(ans_bytes))
        if not ans_bytes[:c.N_BYTES_SOM] == c.SOM:
            dprint("'%s' I/O ERROR: No SOM found" % self.name)
            return failed

        # dprint("SOM okay")
        if not len(ans_bytes) == c.N_BYTES_TX_BUFFER:
            dprint("'%s' I/O ERROR: Expected %i bytes but received %i" %
                   (self.name, c.N_BYTES_TX_BUFFER, len(ans_bytes)))
            return failed

        if c.mcu_firmware == "ALIA v0.2.0 VSCODE":

            # ---------------------------
            #       Legacy firmware
            # ---------------------------

            try:
                counter = struct.unpack(c.binfrmt_counter,
                                        ans_bytes[c.byte_slice_counter])
                millis = struct.unpack(c.binfrmt_millis,
                                       ans_bytes[c.byte_slice_millis])
                micros = struct.unpack(c.binfrmt_micros,
                                       ans_bytes[c.byte_slice_micros])
                idx_phase = struct.unpack(
                    c.binfrmt_idx_phase.format(c.BLOCK_SIZE),
                    ans_bytes[c.byte_slice_idx_phase],
                )
                sig_I = struct.unpack(
                    c.binfrmt_sig_I.format(c.BLOCK_SIZE),
                    ans_bytes[c.byte_slice_sig_I],
                )
            except:
                dprint("'%s' I/O ERROR: Can't unpack bytes" % self.name)
                return failed

            # fmt: off
            counter = counter[0]
            millis = millis[0]
            micros = micros[0]
            idx_phase = np.array(idx_phase, dtype=int, order="C")
            sig_I = np.array(sig_I, dtype=float, order="C")
            # fmt: on

            # dprint("%i %i" % (millis, micros))
            t0 = millis * 1000 + micros
            time = t0 + np.arange(0, c.BLOCK_SIZE) * c.SAMPLING_PERIOD * 1e6
            time = np.asarray(time, dtype=float, order="C")

            phi = 2 * np.pi * idx_phase / c.N_LUT

            # DEBUG test: Add artificial phase delay between ref_X/Y and sig_I
            if 0:  # pylint: disable=using-constant-test
                phase_offset_deg = 50
                phi = np.unwrap(phi + phase_offset_deg / 180 * np.pi)

            # Reconstruct `ref_X` and `ref_Y`
            """
            if c.ref_waveform == Waveform.Sine:
            # OVERRIDE: Only `Sine` allowed, because `Square` and `Triangle`
            # can not be garantueed deterministic on both Arduino and Python
            # side due to rounding differences and the problem of computing the
            # correct 90 degrees quadrant `ref_Y`.
            """
            c.ref_RMS_factor = np.sqrt(2)
            ref_X = np.sin(phi)
            ref_Y = np.cos(phi)
            """
            if c.ref_waveform == Waveform.Square:
                c.ref_RMS_factor = 1
                ref_X.fill(-1)
                ref_X[ref_X_phase < (c.N_LUT / 4.0)] = 1.0
                ref_X[ref_X_phase >= (c.N_LUT / 4.0 * 3.0)] = 1.0
                ref_Y.fill(-1)
                ref_Y[ref_X_phase < (c.N_LUT / 2.0)] = 1.0
                ref_Y[ref_X_phase == (c.N_LUT / 2.0)] = 0.0

            if c.ref_waveform == Waveform.Triangle:
                c.ref_RMS_factor = np.sqrt(3)
                ref_X = np.arcsin(ref_X) / np.pi * 2
                ref_Y = np.arcsin(ref_Y) / np.pi * 2
            """

            # Scale the LUTs [-1, 1] with the RMS factor
            np.multiply(ref_X, c.ref_RMS_factor, out=ref_X)
            np.multiply(ref_Y, c.ref_RMS_factor, out=ref_Y)

            # Transform `sig_I` from [bits] to [V]
            sig_I = np.multiply(sig_I, c.ADC_BITS_TO_V, out=sig_I)

        else:

            # ---------------------------
            #       Modern firmware
            # ---------------------------

            try:
                counter = struct.unpack(c.binfrmt_counter,
                                        ans_bytes[c.byte_slice_counter])
                millis = struct.unpack(c.binfrmt_millis,
                                       ans_bytes[c.byte_slice_millis])
                micros = struct.unpack(c.binfrmt_micros,
                                       ans_bytes[c.byte_slice_micros])
                idx_phase = struct.unpack(c.binfrmt_idx_phase,
                                          ans_bytes[c.byte_slice_idx_phase])
                sig_I = struct.unpack(
                    c.binfrmt_sig_I.format(c.BLOCK_SIZE),
                    ans_bytes[c.byte_slice_sig_I],
                )
            except:
                dprint("'%s' I/O ERROR: Can't unpack bytes" % self.name)
                return failed

            # fmt: off
            counter = counter[0]
            millis = millis[0]
            micros = micros[0]
            idx_phase = idx_phase[0]
            sig_I = np.array(sig_I, dtype=float, order="C")
            # fmt: on

            # dprint("%i %i" % (millis, micros))
            t0 = millis * 1000 + micros
            time = t0 + np.arange(0, c.BLOCK_SIZE) * c.SAMPLING_PERIOD * 1e6
            time = np.asarray(time, dtype=float, order="C")

            # DEBUG test: Add artificial phase delay between ref_X/Y and sig_I
            if 0:  # pylint: disable=using-constant-test
                phase_offset_deg = 10
                idx_phase += int(np.floor(phase_offset_deg / 360 * c.N_LUT))

            # Construct `ref_X` and `ref_Y`
            LUT_X = np.roll(c.LUT_X, -idx_phase)
            LUT_Y = np.roll(c.LUT_Y, -idx_phase)

            ref_X_tiled = np.tile(LUT_X, int(np.ceil(c.BLOCK_SIZE / c.N_LUT)))
            ref_Y_tiled = np.tile(LUT_Y, int(np.ceil(c.BLOCK_SIZE / c.N_LUT)))

            ref_X = np.asarray(
                ref_X_tiled[:c.BLOCK_SIZE],
                dtype=float,
                order="C",
            )

            ref_Y = np.asarray(
                ref_Y_tiled[:c.BLOCK_SIZE],
                dtype=float,
                order="C",
            )

            # Transform `sig_I` from [bits] to [V]
            sig_I = np.multiply(sig_I, c.ADC_BITS_TO_V, out=sig_I)

        return True, counter, time, ref_X, ref_Y, sig_I
Example #25
0
def lockin_DAQ_update():
    """Listen for new data blocks send by the lock-in amplifier and perform the
    main mathematical operations for signal processing. This function will run
    in a dedicated thread (i.e. `worker_DAQ`), separated from the main program
    thread that handles the GUI.
    NOTE: NO GUI OPERATIONS ARE ALLOWED HERE. Otherwise it may affect the
    `worker_DAQ` thread negatively, resulting in lost blocks of data.
    """
    # Shorthands
    c: Alia.Config = alia.config
    state: Alia_qdev.State = alia_qdev.state

    # Prevent throwings errors if just paused
    if alia.lockin_paused:
        return False

    if DEBUG_TIMING:
        tock = Time.perf_counter()
        print("%.2f _DAQ" % (tock - alia.tick))
        alia.tick = tock

    # Listen for data buffers send by the lock-in
    (
        success,
        _counter,
        state.time,
        state.ref_X,
        state.ref_Y,
        state.sig_I,
    ) = alia.listen_to_lockin_amp()

    if not success:
        dprint("@ %s %s" % current_date_time_strings())
        return False

    # Detect dropped blocks
    # ---------------------
    # TODO: Rethink this procedure. Might be easier done with the index of the
    # block that also gets send by the Arduino. We either receive a full block,
    # or we don't. There are no partial blocks that can be received.

    alia_qdev.state.blocks_received += 1
    last_time = state.rb_time[-1] if state.blocks_received > 1 else np.nan
    dT = (state.time[0] - last_time) / 1e6  # [usec] to [sec]

    if dT > c.SAMPLING_PERIOD * 1e6 * 1.10:  # Allow a little clock jitter
        N_dropped_samples = int(round(dT / c.SAMPLING_PERIOD) - 1)
        dprint("Dropped samples: %i" % N_dropped_samples)
        dprint("@ %s %s" % current_date_time_strings())

        # Replace dropped samples with np.nan samples.
        # As a result, the filter output will contain a continuous series of
        # np.nan values in the output for up to `RingBuffer_FIR_Filter.
        # T_settle_filter` seconds long after the occurrence of the last dropped
        # sample.
        state.rb_time.extend(last_time + np.arange(1, N_dropped_samples + 1) *
                             c.SAMPLING_PERIOD * 1e6)
        state.rb_ref_X.extend(np.full(N_dropped_samples, np.nan))
        state.rb_ref_Y.extend(np.full(N_dropped_samples, np.nan))
        state.rb_sig_I.extend(np.full(N_dropped_samples, np.nan))

    # Stage 0
    # -------

    state.sig_I_min = np.min(state.sig_I)
    state.sig_I_max = np.max(state.sig_I)
    state.sig_I_avg = np.mean(state.sig_I)
    state.sig_I_std = np.std(state.sig_I)

    state.rb_time.extend(state.time)
    state.rb_ref_X.extend(state.ref_X)
    state.rb_ref_Y.extend(state.ref_Y)
    state.rb_sig_I.extend(state.sig_I)

    # Note: `ref_X` [non-dim] is transformed to `ref_X*` [V]
    # Note: `ref_Y` [non-dim] is transformed to `ref_Y*` [V]
    window.hcc_ref_X.extendData(
        state.time,
        np.multiply(state.ref_X, c.ref_V_ampl_RMS) + c.ref_V_offset)
    window.hcc_ref_Y.extendData(
        state.time,
        np.multiply(state.ref_Y, c.ref_V_ampl_RMS) + c.ref_V_offset)
    window.hcc_sig_I.extendData(state.time, state.sig_I)

    # Stage 1
    # -------
    # fmt: off

    # Apply filter 1 to sig_I
    state.filt_I = alia_qdev.firf_1_sig_I.apply_filter(state.rb_sig_I)

    if alia_qdev.firf_1_sig_I.filter_has_settled:
        # Retrieve the block of original data from the past that aligns with
        # the current filter output
        valid_slice = alia_qdev.firf_1_sig_I.rb_valid_slice

        state.time_1 = state.rb_time[valid_slice]
        old_sig_I = state.rb_sig_I[valid_slice]
        old_ref_X = state.rb_ref_X[valid_slice]
        old_ref_Y = state.rb_ref_Y[valid_slice]

        # Heterodyne mixing
        np.multiply(state.filt_I, old_ref_X, out=state.mix_X)
        np.multiply(state.filt_I, old_ref_Y, out=state.mix_Y)
    else:
        state.time_1.fill(np.nan)
        old_sig_I = np.full(c.BLOCK_SIZE, np.nan)
        state.mix_X.fill(np.nan)
        state.mix_Y.fill(np.nan)

    state.filt_I_min = np.min(state.filt_I)
    state.filt_I_max = np.max(state.filt_I)
    state.filt_I_avg = np.mean(state.filt_I)
    state.filt_I_std = np.std(state.filt_I)

    state.rb_time_1.extend(state.time_1)
    state.rb_filt_I.extend(state.filt_I)
    state.rb_mix_X.extend(state.mix_X)
    state.rb_mix_Y.extend(state.mix_Y)

    window.hcc_filt_1_in.extendData(state.time_1, old_sig_I)
    window.hcc_filt_1_out.extendData(state.time_1, state.filt_I)
    window.hcc_mix_X.extendData(state.time_1, state.mix_X)
    window.hcc_mix_Y.extendData(state.time_1, state.mix_Y)
    # fmt: on

    # Stage 2
    # -------

    # Apply filter 2 to the mixer output
    state.X = alia_qdev.firf_2_mix_X.apply_filter(state.rb_mix_X)
    state.Y = alia_qdev.firf_2_mix_Y.apply_filter(state.rb_mix_Y)

    if alia_qdev.firf_2_mix_X.filter_has_settled:
        # Retrieve the block of time data from the past that aligns with
        # the current filter output
        valid_slice = alia_qdev.firf_1_sig_I.rb_valid_slice
        state.time_2 = state.rb_time_1[valid_slice]

        # Signal amplitude: R
        np.sqrt(np.add(np.square(state.X), np.square(state.Y)), out=state.R)

        # Signal phase: Theta
        np.arctan2(state.Y, state.X, out=state.T)
        np.multiply(state.T, 180 / np.pi, out=state.T)  # [rad] to [deg]
    else:
        state.time_2.fill(np.nan)
        state.R.fill(np.nan)
        state.T.fill(np.nan)

    state.X_avg = np.mean(state.X)
    state.Y_avg = np.mean(state.Y)
    state.R_avg = np.mean(state.R)
    state.T_avg = np.mean(state.T)

    state.rb_time_2.extend(state.time_2)
    state.rb_X.extend(state.X)
    state.rb_Y.extend(state.Y)
    state.rb_R.extend(state.R)
    state.rb_T.extend(state.T)

    window.hcc_LIA_XR.extendData(
        state.time_2, state.X if window.qrbt_XR_X.isChecked() else state.R)
    window.hcc_LIA_YT.extendData(
        state.time_2, state.Y if window.qrbt_YT_Y.isChecked() else state.T)

    # Check if memory address of underlying buffer is still unchanged
    # pylint: disable=pointless-string-statement
    """
    test = np.asarray(state.rb_X)
    print("%6i, mem: %i, cont?: %i, rb buf mem: %i, full? %i" % (
            state.blocks_received,
            test.__array_interface__['data'][0],
            test.flags['C_CONTIGUOUS'],
            state.rb_X._unwrap_buffer.__array_interface__['data'][0],
            state.rb_X.is_full))
    """

    # Power spectra
    # -------------

    calculate_PS_sig_I()
    calculate_PS_filt_I()
    calculate_PS_mix_X()
    calculate_PS_mix_Y()
    calculate_PS_R()

    # Logging to file
    logger.update(mode="w")

    # Return success
    return True