def init(self):
        # Prevent interference between the read callback and the shutdown code:
        self.tasklock = threading.RLock()

        # Assigned on a per-task basis and cleared afterward:
        self.read_array = None
        self.task = None

        # Assigned on a per-shot basis and cleared afterward:
        self.buffered_mode = False
        self.h5_file = None
        self.acquired_data = None
        self.buffered_rate = None
        self.buffered_chans = None

        # Hard coded for now. Perhaps we will add functionality to enable
        # and disable inputs in manual mode, and adjust the rate:
        self.manual_mode_chans = ['ai%d' % i for i in range(self.num_AI)]
        self.manual_mode_rate = 1000

        # An event for knowing when the wait durations are known, so that we may use
        # them to chunk up acquisition data:
        self.wait_durations_analysed = Event('wait_durations_analysed')

        # Start task for manual mode
        self.start_task(self.manual_mode_chans, self.manual_mode_rate)
 def __init__(self, initial_settings):
     self.menu = None
     self.notifications = {}
     self.initial_settings = initial_settings
     self.BLACS = None
     self.command_queue = Queue()
     self.master_pseudoclock = None
     self.shot_start_time = None
     self.stop_time = None
     self.markers = None
     self.waits = None
     self.time_spent_waiting = None
     self.next_wait_index = None
     self.next_marker_index = None
     self.bar_text_prefix = None
     self.h5_filepath = None
     self.wait_completed_events_supported = False
     self.wait_completed = Event('wait_completed', type='wait')
     self.mainloop_thread = threading.Thread(target=self.mainloop)
     self.mainloop_thread.daemon = True
     self.mainloop_thread.start()
    def init(self):

        self.all_waits_finished = Event('all_waits_finished', type='post')
        self.wait_durations_analysed = Event('wait_durations_analysed',
                                             type='post')
        self.wait_completed = Event('wait_completed', type='post')

        # Set on a per-shot basis and cleared afterward:
        self.h5_file = None
        self.CI_task = None
        self.DO_task = None
        self.wait_table = None
        self.semiperiods = None
        self.wait_monitor_thread = None

        # Saved error in case one occurs in the thread, we can raise it later in
        # transition_to_manual:
        self.wait_monitor_thread_exception = None
        # To trigger early shutdown of the wait monitor thread:
        self.shutting_down = False

        # Does this device have the "incomplete sample detection" feature? This
        # determines whether the first sample on our semiperiod counter input task will
        # be automatically discarded before we see it, or whether we will have to
        # discard it ourselves
        self.incomplete_sample_detection = incomplete_sample_detection(
            self.MAX_name)

        # Data for timeout triggers:
        if self.timeout_trigger_type == 'rising':
            trigger_value = 1
            rearm_value = 0
        elif self.timeout_trigger_type == 'falling':
            trigger_value = 0
            rearm_value = 1
        else:
            msg = 'timeout_trigger_type  must be "rising" or "falling", not "{}".'
            raise ValueError(msg.format(self.timeout_trigger_type))
        self.timeout_trigger = np.array([trigger_value], dtype=np.uint8)
        self.timeout_rearm = np.array([rearm_value], dtype=np.uint8)
class NI_DAQmxWaitMonitorWorker(Worker):
    def init(self):

        self.all_waits_finished = Event('all_waits_finished', type='post')
        self.wait_durations_analysed = Event('wait_durations_analysed', type='post')
        self.wait_completed = Event('wait_completed', type='post')

        # Set on a per-shot basis and cleared afterward:
        self.h5_file = None
        self.CI_task = None
        self.DO_task = None
        self.wait_table = None
        self.semiperiods = None
        self.wait_monitor_thread = None

        # Saved error in case one occurs in the thread, we can raise it later in
        # transition_to_manual:
        self.wait_monitor_thread_exception = None
        # To trigger early shutdown of the wait monitor thread:
        self.shutting_down = False

        # Does this device have the "incomplete sample detection" feature? This
        # determines whether the first sample on our semiperiod counter input task will
        # be automatically discarded before we see it, or whether we will have to
        # discard it ourselves
        self.incomplete_sample_detection = incomplete_sample_detection(self.MAX_name)

        # Data for timeout triggers:
        if self.timeout_trigger_type == 'rising':
            trigger_value = 1
            rearm_value = 0
        elif self.timeout_trigger_type == 'falling':
            trigger_value = 0
            rearm_value = 1
        else:
            msg = 'timeout_trigger_type  must be "rising" or "falling", not "{}".'
            raise ValueError(msg.format(self.timeout_trigger_type))
        self.timeout_trigger = np.array([trigger_value], dtype=np.uint8)
        self.timeout_rearm = np.array([rearm_value], dtype=np.uint8)

    def shutdown(self):
        self.stop_tasks(True)

    def read_edges(self, npts, timeout=None):
        """Wait up to the given timeout in seconds for an edge on the wait monitor and
        and return the duration since the previous edge. Return None upon timeout."""
        samples_read = int32()
        # If no timeout, call read repeatedly with a 0.2 second timeout to ensure we
        # don't block indefinitely and can still abort.
        if timeout is None:
            read_timeout = 0.2
        else:
            read_timeout = timeout
        read_array = np.zeros(npts)
        while True:
            if self.shutting_down:
                raise RuntimeError('Stopped before expected number of samples acquired')
            try:
                self.CI_task.ReadCounterF64(
                    npts, read_timeout, read_array, npts, samples_read, None
                )
            except SamplesNotYetAvailableError:
                if timeout is None:
                    continue
                return None
            return read_array

    def wait_monitor(self):
        try:
            # Read edge times from the counter input task, indiciating the times of the
            # pulses that occur at the start of the experiment and after every wait. If a
            # timeout occurs, pulse the timeout output to force a resume of the master
            # pseudoclock. Save the resulting
            self.logger.debug('Wait monitor thread starting')
            with self.kill_lock:
                self.logger.debug('Waiting for start of experiment')
                # Wait for the pulse indicating the start of the experiment:
                if self.incomplete_sample_detection:
                    semiperiods = self.read_edges(1, timeout=None)
                else:
                    semiperiods = self.read_edges(2, timeout=None)
                self.logger.debug('Experiment started, got edges:' + str(semiperiods))
                # May have been one or two edges, depending on whether the device has
                # incomplete sample detection. We are only interested in the second one
                # anyway, it tells us how long the initial pulse was. Store the pulse width
                # for later, we will use it for making timeout pulses if necessary. Note
                # that the variable current_time is labscript time, so it will be reset
                # after each wait to the time of that wait plus pulse_width.
                current_time = pulse_width = semiperiods[-1]
                self.semiperiods.append(semiperiods[-1])
                # Alright, we're now a short way into the experiment.
                for wait in self.wait_table:
                    # How long until when the next wait should timeout?
                    timeout = wait['time'] + wait['timeout'] - current_time
                    timeout = max(timeout, 0)  # ensure non-negative
                    # Wait that long for the next pulse:
                    self.logger.debug('Waiting for pulse indicating end of wait')
                    semiperiods = self.read_edges(2, timeout)
                    # Did the wait finish of its own accord, or time out?
                    if semiperiods is None:
                        # It timed out. Better trigger the clock to resume!
                        msg = """Wait timed out; retriggering clock with {:.3e} s pulse
                            ({} edge)"""
                        msg = dedent(msg).format(pulse_width, self.timeout_trigger_type)
                        self.logger.debug(msg)
                        self.send_resume_trigger(pulse_width)
                        # Wait for it to respond to that:
                        self.logger.debug('Waiting for pulse indicating end of wait')
                        semiperiods = self.read_edges(2, timeout=None)
                    # Alright, now we're at the end of the wait.
                    self.semiperiods.extend(semiperiods)
                    self.logger.debug('Wait completed')
                    current_time = wait['time'] + semiperiods[-1]
                    # Inform any interested parties that a wait has completed:
                    postdata = _ensure_str(wait['label'])
                    self.wait_completed.post(self.h5_file, data=postdata)
                # Inform any interested parties that waits have all finished:
                self.logger.debug('All waits finished')
                self.all_waits_finished.post(self.h5_file)
        except Exception:
            self.logger.exception('Exception in wait monitor thread:')
            # Save the exception so it can be raised in transition_to_manual
            self.wait_monitor_thread_exception = sys.exc_info()

    def send_resume_trigger(self, pulse_width):
        written = int32()
        # Trigger:
        self.DO_task.WriteDigitalLines(
            1, True, 1, DAQmx_Val_GroupByChannel, self.timeout_trigger, written, None
        )
        # Wait however long we observed the first pulse of the experiment to be. In
        # practice this is likely to be negligible compared to the other software delays
        # here, but in case it is larger we'd better wait:
        time.sleep(pulse_width)
        # Rearm trigger:
        self.DO_task.WriteDigitalLines(
            1, True, 1, DAQmx_Val_GroupByChannel, self.timeout_rearm, written, None
        )

    def stop_tasks(self, abort):
        self.logger.debug('stop_tasks')
        if self.wait_monitor_thread is not None:
            if abort:
                # This will cause the wait_monitor thread to raise an exception within a
                # short time, allowing us to join it before it would otherwise be done.
                self.shutting_down = True
            self.wait_monitor_thread.join()
            self.wait_monitor_thread = None
            self.shutting_down = False
            if not abort and self.wait_monitor_thread_exception is not None:
                # Raise any unexpected errors from the wait monitor thread:
                _reraise(*self.wait_monitor_thread_exception)
            self.wait_monitor_thread_exception = None
            if not abort:
                # Don't want errors about incomplete task to be raised if we are aborting:
                self.CI_task.StopTask()
            self.DO_task.StopTask()
        if self.CI_task is not None:
            self.CI_task.ClearTask()
            self.CI_task = None
        if self.DO_task is not None:
            self.DO_task.ClearTask()
            self.DO_task = None
        self.logger.debug('finished stop_tasks')

    def start_tasks(self):

        # The counter acquisition task:
        self.CI_task = Task()
        CI_chan = self.MAX_name + '/' + self.wait_acq_connection
        # What is the longest time in between waits, plus the timeout of the
        # second wait?
        interwait_times = np.diff([0] + list(self.wait_table['time']))
        max_measure_time = max(interwait_times + self.wait_table['timeout'])
        # Allow for software delays in timeouts.
        max_measure_time += 1.0
        min_measure_time = self.min_semiperiod_measurement
        self.logger.debug(
            "CI measurement range is: min: %f max: %f",
            min_measure_time,
            max_measure_time,
        )
        self.CI_task.CreateCISemiPeriodChan(
            CI_chan, '', min_measure_time, max_measure_time, DAQmx_Val_Seconds, ""
        )
        num_edges = 2 * (len(self.wait_table) + 1)
        self.CI_task.CfgImplicitTiming(DAQmx_Val_ContSamps, num_edges)
        self.CI_task.StartTask()

        # The timeout task:
        self.DO_task = Task()
        DO_chan = self.MAX_name + '/' + self.wait_timeout_connection
        self.DO_task.CreateDOChan(DO_chan, "", DAQmx_Val_ChanForAllLines)
        # Ensure timeout trigger is armed:
        written = int32()
        # Writing autostarts the task:
        self.DO_task.WriteDigitalLines(
            1, True, 1, DAQmx_Val_GroupByChannel, self.timeout_rearm, written, None
        )

    def transition_to_buffered(self, device_name, h5file, initial_values, fresh):
        self.logger.debug('transition_to_buffered')
        self.h5_file = h5file
        with h5py.File(h5file, 'r') as hdf5_file:
            dataset = hdf5_file['waits']
            if len(dataset) == 0:
                # There are no waits. Do nothing.
                self.logger.debug('There are no waits, not transitioning to buffered')
                self.wait_table = None
                return {}
            self.wait_table = dataset[:]

        self.start_tasks()

        # An array to store the results of counter acquisition:
        self.semiperiods = []
        self.wait_monitor_thread = threading.Thread(target=self.wait_monitor)
        # Not a daemon thread, as it implements wait timeouts - we need it to stay alive
        # if other things die.
        self.wait_monitor_thread.start()
        self.logger.debug('finished transition to buffered')

        return {}

    def transition_to_manual(self, abort=False):
        self.logger.debug('transition_to_manual')
        self.stop_tasks(abort)
        if not abort and self.wait_table is not None:
            # Let's work out how long the waits were. The absolute times of each edge on
            # the wait monitor were:
            edge_times = np.cumsum(self.semiperiods)
            # Now there was also a rising edge at t=0 that we didn't measure:
            edge_times = np.insert(edge_times, 0, 0)
            # Ok, and the even-indexed ones of these were rising edges.
            rising_edge_times = edge_times[::2]
            # Now what were the times between rising edges?
            periods = np.diff(rising_edge_times)
            # How does this compare to how long we expected there to be between the
            # start of the experiment and the first wait, and then between each pair of
            # waits? The difference will give us the waits' durations.
            resume_times = self.wait_table['time']
            # Again, include the start of the experiment, t=0:
            resume_times = np.insert(resume_times, 0, 0)
            run_periods = np.diff(resume_times)
            wait_durations = periods - run_periods
            waits_timed_out = wait_durations > self.wait_table['timeout']

            # Work out how long the waits were, save them, post an event saying so:
            dtypes = [
                ('label', 'a256'),
                ('time', float),
                ('timeout', float),
                ('duration', float),
                ('timed_out', bool),
            ]
            data = np.empty(len(self.wait_table), dtype=dtypes)
            data['label'] = self.wait_table['label']
            data['time'] = self.wait_table['time']
            data['timeout'] = self.wait_table['timeout']
            data['duration'] = wait_durations
            data['timed_out'] = waits_timed_out
            with h5py.File(self.h5_file, 'a') as hdf5_file:
                hdf5_file.create_dataset('/data/waits', data=data)
            self.wait_durations_analysed.post(self.h5_file)

        self.h5_file = None
        self.semiperiods = None
        return True

    def abort_buffered(self):
        return self.transition_to_manual(True)

    def abort_transition_to_buffered(self):
        return self.transition_to_manual(True)

    def program_manual(self, values):
        return {}
class NI_DAQmxAcquisitionWorker(Worker):
    MAX_READ_INTERVAL = 0.2
    MAX_READ_PTS = 10000

    def init(self):
        # Prevent interference between the read callback and the shutdown code:
        self.tasklock = threading.RLock()

        # Assigned on a per-task basis and cleared afterward:
        self.read_array = None
        self.task = None

        # Assigned on a per-shot basis and cleared afterward:
        self.buffered_mode = False
        self.h5_file = None
        self.acquired_data = None
        self.buffered_rate = None
        self.buffered_chans = None

        # Hard coded for now. Perhaps we will add functionality to enable
        # and disable inputs in manual mode, and adjust the rate:
        self.manual_mode_chans = ['ai%d' % i for i in range(self.num_AI)]
        self.manual_mode_rate = 1000

        # An event for knowing when the wait durations are known, so that we may use
        # them to chunk up acquisition data:
        self.wait_durations_analysed = Event('wait_durations_analysed')

        # Start task for manual mode
        self.start_task(self.manual_mode_chans, self.manual_mode_rate)

    def shutdown(self):
        if self.task is not None:
            self.stop_task()

    def read(self, task_handle, event_type, num_samples, callback_data=None):
        """Called as a callback by DAQmx while task is running. Also called by us to get
        remaining data just prior to stopping the task. Since the callback runs
        in a separate thread, we need to serialise access to instance variables"""
        samples_read = int32()
        with self.tasklock:
            if self.task is None or task_handle != self.task.taskHandle.value:
                # Task stopped already.
                return 0
            self.task.ReadAnalogF64(
                num_samples,
                -1,
                DAQmx_Val_GroupByScanNumber,
                self.read_array,
                self.read_array.size,
                samples_read,
                None,
            )
            # Select only the data read, and downconvert to 32 bit:
            data = self.read_array[: int(samples_read.value), :].astype(np.float32)
            if self.buffered_mode:
                # Append to the list of acquired data:
                self.acquired_data.append(data)
            else:
                # TODO: Send it to the broker thingy.
                pass
        return 0

    def start_task(self, chans, rate):
        """Set up a task that acquires data with a callback every MAX_READ_PTS points or
        MAX_READ_INTERVAL seconds, whichever is faster. NI DAQmx calls callbacks in a
        separate thread, so this method returns, but data acquisition continues until
        stop_task() is called. Data is appended to self.acquired_data if
        self.buffered_mode=True, or (TODO) sent to the [whatever the AI server broker is
        called] if self.buffered_mode=False."""

        if self.task is not None:
            raise RuntimeError('Task already running')

        if chans is None:
            return

        # Get data MAX_READ_PTS points at a time or once every MAX_READ_INTERVAL
        # seconds, whichever is faster:
        num_samples = min(self.MAX_READ_PTS, int(rate * self.MAX_READ_INTERVAL))

        self.read_array = np.zeros((num_samples, len(chans)), dtype=np.float64)
        self.task = Task()

        for chan in chans:
            self.task.CreateAIVoltageChan(
                self.MAX_name + '/' + chan,
                "",
                DAQmx_Val_RSE,
                self.AI_range[0],
                self.AI_range[1],
                DAQmx_Val_Volts,
                None,
            )

        self.task.CfgSampClkTiming(
            "", rate, DAQmx_Val_Rising, DAQmx_Val_ContSamps, num_samples
        )
        if self.buffered_mode:
            self.task.CfgDigEdgeStartTrig(self.clock_terminal, DAQmx_Val_Rising)

        # This must not be garbage collected until the task is:
        self.task.callback_ptr = DAQmxEveryNSamplesEventCallbackPtr(self.read)

        self.task.RegisterEveryNSamplesEvent(
            DAQmx_Val_Acquired_Into_Buffer, num_samples, 0, self.task.callback_ptr, 100
        )

        self.task.StartTask()

    def stop_task(self):
        with self.tasklock:
            if self.task is None:
                raise RuntimeError('Task not running')
            # Read remaining data:
            self.read(self.task, None, -1)
            # Stop the task:
            self.task.StopTask()
            self.task.ClearTask()
            self.task = None
            self.read_array = None

    def transition_to_buffered(self, device_name, h5file, initial_values, fresh):
        self.logger.debug('transition_to_buffered')

        # read channels, acquisition rate, etc from H5 file
        with h5py.File(h5file, 'r') as f:
            group = f['/devices/' + device_name]
            if 'AI' not in group:
                # No acquisition
                return {}
            AI_table = group['AI'][:]
            device_properties = properties.get(f, device_name, 'device_properties')

        chans = [_ensure_str(c) for c in AI_table['connection']]
        # Remove duplicates and sort:
        if chans:
            self.buffered_chans = sorted(set(chans), key=split_conn_AI)
        self.h5_file = h5file
        self.buffered_rate = device_properties['acquisition_rate']
        self.acquired_data = []
        # Stop the manual mode task and start the buffered mode task:
        self.stop_task()
        self.buffered_mode = True
        self.start_task(self.buffered_chans, self.buffered_rate)
        return {}

    def transition_to_manual(self, abort=False):
        self.logger.debug('transition_to_manual')
        #  If we were doing buffered mode acquisition, stop the buffered mode task and
        # start the manual mode task. We might not have been doing buffered mode
        # acquisition if abort() was called when we are not in buffered mode, or if
        # there were no acuisitions this shot.
        if not self.buffered_mode:
            return True
        if self.buffered_chans is not None:
            self.stop_task()
        self.buffered_mode = False
        self.logger.info('transitioning to manual mode, task stopped')
        self.start_task(self.manual_mode_chans, self.manual_mode_rate)

        if abort:
            self.acquired_data = None
            self.buffered_chans = None
            self.h5_file = None
            self.buffered_rate = None
            return True

        with h5py.File(self.h5_file, 'a') as hdf5_file:
            data_group = hdf5_file['data']
            data_group.create_group(self.device_name)
            waits_in_use = len(hdf5_file['waits']) > 0

        if self.buffered_chans is not None and not self.acquired_data:
            msg = """No data was acquired. Perhaps the acquisition task was not
                triggered to start, is the device connected to a pseudoclock?"""
            raise RuntimeError(dedent(msg))
        # Concatenate our chunks of acquired data and recast them as a structured
        # array with channel names:
        if self.acquired_data:
            start_time = time.time()
            dtypes = [(chan, np.float32) for chan in self.buffered_chans]
            raw_data = np.concatenate(self.acquired_data).view(dtypes)
            raw_data = raw_data.reshape((len(raw_data),))
            self.acquired_data = None
            self.buffered_chans = None
            self.extract_measurements(raw_data, waits_in_use)
            self.h5_file = None
            self.buffered_rate = None
            msg = 'data written, time taken: %ss' % str(time.time() - start_time)
        else:
            msg = 'No acquisitions in this shot.'
        self.logger.info(msg)

        return True

    def extract_measurements(self, raw_data, waits_in_use):
        self.logger.debug('extract_measurements')
        if waits_in_use:
            # There were waits in this shot. We need to wait until the other process has
            # determined their durations before we proceed:
            self.wait_durations_analysed.wait(self.h5_file)

        with h5py.File(self.h5_file, 'a') as hdf5_file:
            if waits_in_use:
                # get the wait start times and durations
                waits = hdf5_file['/data/waits']
                wait_times = waits['time']
                wait_durations = waits['duration']
            try:
                acquisitions = hdf5_file['/devices/' + self.device_name + '/AI']
            except KeyError:
                # No acquisitions!
                return
            try:
                measurements = hdf5_file['/data/traces']
            except KeyError:
                # Group doesn't exist yet, create it:
                measurements = hdf5_file.create_group('/data/traces')

            t0 = self.AI_start_delay
            for connection, label, t_start, t_end, _, _, _ in acquisitions:
                connection = _ensure_str(connection)
                label = _ensure_str(label)
                if waits_in_use:
                    # add durations from all waits that start prior to t_start of
                    # acquisition
                    t_start += wait_durations[(wait_times < t_start)].sum()
                    # compare wait times to t_end to allow for waits during an
                    # acquisition
                    t_end += wait_durations[(wait_times < t_end)].sum()
                i_start = int(np.ceil(self.buffered_rate * (t_start - t0)))
                i_end = int(np.floor(self.buffered_rate * (t_end - t0)))
                # np.ceil does what we want above, but float errors can miss the
                # equality:
                if t0 + (i_start - 1) / self.buffered_rate - t_start > -2e-16:
                    i_start -= 1
                # We want np.floor(x) to yield the largest integer < x (not <=):
                if t_end - t0 - i_end / self.buffered_rate < 2e-16:
                    i_end -= 1
                t_i = t0 + i_start / self.buffered_rate
                t_f = t0 + i_end / self.buffered_rate
                times = np.linspace(t_i, t_f, i_end - i_start + 1, endpoint=True)
                values = raw_data[connection][i_start : i_end + 1]
                dtypes = [('t', np.float64), ('values', np.float32)]
                data = np.empty(len(values), dtype=dtypes)
                data['t'] = times
                data['values'] = values
                measurements.create_dataset(label, data=data)

    def abort_buffered(self):
        return self.transition_to_manual(True)

    def abort_transition_to_buffered(self):
        return self.transition_to_manual(True)

    def program_manual(self, values):
        return {}
class Plugin(object):
    def __init__(self, initial_settings):
        self.menu = None
        self.notifications = {}
        self.initial_settings = initial_settings
        self.BLACS = None
        self.command_queue = Queue()
        self.master_pseudoclock = None
        self.shot_start_time = None
        self.stop_time = None
        self.markers = None
        self.waits = None
        self.time_spent_waiting = None
        self.next_wait_index = None
        self.next_marker_index = None
        self.bar_text_prefix = None
        self.h5_filepath = None
        self.wait_completed_events_supported = False
        self.wait_completed = Event('wait_completed', type='wait')
        self.mainloop_thread = threading.Thread(target=self.mainloop)
        self.mainloop_thread.daemon = True
        self.mainloop_thread.start()
        
    def plugin_setup_complete(self, BLACS):
        self.BLACS = BLACS
        self.ui = UiLoader().load(os.path.join(PLUGINS_DIR, module, 'controls.ui'))
        self.bar = self.ui.bar
        self.style = QtWidgets.QStyleFactory.create('Fusion')
        if self.style is None:
            # If we're on Qt4, fall back to Plastique style:
            self.style = QtWidgets.QStyleFactory.create('Plastique')
        if self.style is None:
            # Not sure what's up, but fall back to app's default style:
            self.style = QtWidgets.QApplication.style()
        self.bar.setStyle(self.style)
        self.bar.setMaximum(BAR_MAX)
        self.bar.setAlignment(QtCore.Qt.AlignCenter)
        # Add our controls to the BLACS gui:
        BLACS['ui'].queue_status_verticalLayout.insertWidget(0, self.ui)
        # We need to know the name of the master pseudoclock so we can look up
        # the duration of each shot:
        self.master_pseudoclock = self.BLACS['experiment_queue'].master_pseudoclock

        # Check if the wait monitor device, if any, supports wait completed events:
        with h5py.File(self.BLACS['connection_table_h5file'], 'r') as f:
            if 'waits' in f:
                acq_device = f['waits'].attrs['wait_monitor_acquisition_device']
                acq_device = _ensure_str(acq_device)
                if acq_device:
                    props = properties.get(f, acq_device, 'connection_table_properties')
                    if props.get('wait_monitor_supports_wait_completed_events', False):
                        self.wait_completed_events_supported = True

        self.ui.wait_warning.hide()

    def get_save_data(self):
        return {}
    
    def get_callbacks(self):
        return {'science_over': self.on_science_over,
                'science_starting': self.on_science_starting}
        
    @callback(priority=100)
    def on_science_starting(self, h5_filepath):
        # Tell the mainloop that we're starting a shot:
        self.command_queue.put(('start', h5_filepath))

    @callback(priority=5)
    def on_science_over(self, h5_filepath):
        # Tell the mainloop we're done with this shot:
        self.command_queue.put(('stop', None))

    @inmain_decorator(True)
    def clear_bar(self):
        self.bar.setEnabled(False)
        self.bar.setFormat('No shot running')
        self.bar.setValue(0)
        self.bar.setPalette(self.style.standardPalette())
        self.ui.wait_warning.hide()

    def get_next_thing(self):
        """Figure out what's going to happen next: a wait, a time marker, or a
        regular update. Return a string saying which, and a float saying how
        long from now it will occur. If the thing has already happened but not
        been taken into account by our processing yet, then return zero for
        the time."""
        if self.waits is not None and self.next_wait_index < len(self.waits):
            next_wait_time = self.waits['time'][self.next_wait_index]
        else:
            next_wait_time = np.inf
        if self.markers is not None and self.next_marker_index < len(self.markers):
            next_marker_time = self.markers['time'][self.next_marker_index]
        else:
            next_marker_time = np.inf
        assert self.shot_start_time is not None
        assert self.time_spent_waiting is not None
        labscript_time = time.time() - self.shot_start_time - self.time_spent_waiting
        next_update_time = labscript_time + UPDATE_INTERVAL
        if next_update_time < next_wait_time and next_update_time < next_marker_time:
            return 'update', UPDATE_INTERVAL
        elif next_wait_time < next_marker_time:
            return 'wait', max(0, next_wait_time - labscript_time)
        else:
            return 'marker', max(0, next_marker_time - labscript_time)

    @inmain_decorator(True)
    def update_bar_style(self, marker=False, wait=False, previous=False):
        """Update the bar's style to reflect the next marker or wait,
        according to self.next_marker_index or self.next_wait_index. If
        previous=True, instead update to reflect the current marker or
        wait."""
        assert not (marker and wait)
        # Ignore requests to reflect markers or waits if there are no markers
        # or waits in this shot:
        marker = marker and self.markers is not None and len(self.markers) > 0
        wait = wait and self.waits is not None and len(self.waits) > 0
        if marker:
            marker_index = self.next_marker_index
            if previous:
                marker_index -= 1
                assert marker_index >= 0
            label, _, color = self.markers[marker_index]
            self.bar_text_prefix = '[%s] ' % _ensure_str(label)
            r, g, b = color[0]
            # Black is the default colour in labscript.add_time_marker.
            # Don't change the bar colour if the marker colour is black.
            if (r, g, b) != (0,0,0):
                bar_color = QtGui.QColor(r, g, b)
                if black_has_good_contrast(r, g, b):
                    highlight_text_color = QtCore.Qt.black
                else:
                    highlight_text_color = QtCore.Qt.white
            else:
                bar_color = None
                highlight_text_color = None
            regular_text_color = None # use default
        elif wait:
            wait_index = self.next_wait_index
            if previous:
                wait_index -= 1
                assert wait_index >= 0
            label = self.waits[wait_index]['label']
            self.bar_text_prefix = '-%s- ' % _ensure_str(label)
            highlight_text_color = regular_text_color = QtGui.QColor(192, 0, 0)
            bar_color = QtCore.Qt.gray
        if marker or wait:
            palette = QtGui.QPalette()
            if bar_color is not None:
                palette.setColor(QtGui.QPalette.Highlight, bar_color)
            # Ensure the colour of the text on the filled in bit of the progress
            # bar has good contrast:
            if highlight_text_color is not None:
                palette.setColor(QtGui.QPalette.HighlightedText, highlight_text_color)
            if regular_text_color is not None:
                palette.setColor(QtGui.QPalette.Text, regular_text_color)
            self.bar.setPalette(palette)
        else:
            self.bar_text_prefix = None
            # Default palette:
            self.bar.setPalette(self.style.standardPalette())

    @inmain_decorator(True)
    def update_bar_value(self, marker=False, wait=False):
        """Update the progress bar with the current time elapsed. If marker or wait is
        true, then use the exact time at which the next marker or wait is defined,
        rather than the current time as returned by time.time()"""
        thinspace = u'\u2009'
        self.bar.setEnabled(True)
        assert not (marker and wait)
        if marker:
            labscript_time = self.markers['time'][self.next_marker_index]
        elif wait:
            labscript_time = self.waits['time'][self.next_wait_index]
        else:
            labscript_time = time.time() - self.shot_start_time - self.time_spent_waiting
        value = int(round(labscript_time / self.stop_time * BAR_MAX))
        self.bar.setValue(value)

        text = u'%.2f%ss / %.2f%ss (%%p%s%%)'
        text = text % (labscript_time, thinspace, self.stop_time, thinspace, thinspace)
        if self.bar_text_prefix is not None:
            text = self.bar_text_prefix + text
        self.bar.setFormat(text)

    def _start(self, h5_filepath):
        """Called from the mainloop when starting a shot"""
        self.h5_filepath = h5_filepath
        # Get the stop time, any waits and any markers from the shot:
        with h5py.File(h5_filepath, 'r') as f:
            props = properties.get(f, self.master_pseudoclock, 'device_properties')
            self.stop_time = props['stop_time']
            try:
                self.markers = f['time_markers'][:]
                self.markers.sort(order=(bytes if PY2 else str)('time'))
            except KeyError:
                self.markers = None
            try:
                self.waits = f['waits'][:]
                self.waits.sort(order=(bytes if PY2 else str)('time'))
            except KeyError:
                self.waits = None
        self.shot_start_time = time.time()
        self.time_spent_waiting = 0
        self.next_marker_index = 0
        self.next_wait_index = 0

    def _stop(self):
        """Called from the mainloop when ending a shot"""
        self.h5_filepath = None
        self.shot_start_time = None
        self.stop_time = None
        self.markers = None
        self.waits = None
        self.time_spent_waiting = None
        self.next_wait_index = None
        self.next_marker_index = None
        self.bar_text_prefix = None

    def mainloop(self):
        running = False
        self.clear_bar()
        while True:
            try:
                if running:
                    # How long until the next thing of interest occurs, and
                    # what is it? It can be either a wait, a marker, or a
                    # regular update.
                    next_thing, timeout = self.get_next_thing()
                    try:
                        command, _ = self.command_queue.get(timeout=timeout)
                    except Empty:
                        if next_thing == 'update':
                            self.update_bar_value()
                        if next_thing == 'marker':
                            self.update_bar_style(marker=True)
                            self.update_bar_value(marker=True)
                            self.next_marker_index += 1
                        elif next_thing == 'wait':
                            wait_start_time = time.time()
                            self.update_bar_style(wait=True)
                            self.update_bar_value(wait=True)
                            self.next_wait_index += 1
                            # wait for the wait to complete, but abandon
                            # processing if the command queue is non-empty,
                            # i.e. if a stop command is sent.
                            while self.command_queue.empty():
                                try:
                                    # Wait for only 0.1 sec at a time, so that
                                    # we can check if the queue is empty in between:
                                    self.wait_completed.wait(self.h5_filepath, timeout=0.1)
                                except TimeoutError:
                                    # Only wait for wait completed events if the wait
                                    # monitor device supports them. Otherwise, skip
                                    # after this first timeout, and it will just look
                                    # like the wait had 0.1 sec duration.
                                    if self.wait_completed_events_supported:
                                        # The wait is still in progress:
                                        continue
                                # The wait completed (or completion events are not
                                # supported):
                                self.time_spent_waiting += time.time() - wait_start_time
                                # Set the bar style back to whatever the
                                # previous marker was, if any:
                                self.update_bar_style(marker=True, previous=True)
                                self.update_bar_value()
                                break
                        continue
                else:
                    command, h5_filepath = self.command_queue.get()
                if command == 'close':
                    break
                elif command == 'start':
                    assert not running
                    running = True
                    self._start(h5_filepath)
                    self.update_bar_value()
                    if (
                        self.waits is not None
                        and len(self.waits) > 0
                        and not self.wait_completed_events_supported
                    ):
                        inmain(self.ui.wait_warning.show)
                elif command == 'stop':
                    assert running
                    self.clear_bar()
                    running = False
                    self._stop()
                else:
                    raise ValueError(command)
            except Exception:
                logger.exception("Exception in mainloop, ignoring.")
                # Stop processing of the current shot, if any.
                self.clear_bar()
                inmain(self.bar.setFormat, "Error in progress bar plugin")
                running = False
                self._stop()
    
    def close(self):
        self.command_queue.put(('close', None))
        self.mainloop_thread.join()

    # The rest of these are boilerplate:
    def get_menu_class(self):
        return None
        
    def get_notification_classes(self):
        return []
        
    def get_setting_classes(self):
        return []
    
    def set_menu_instance(self, menu):
        self.menu = menu
        
    def set_notification_instances(self, notifications):
        self.notifications = notifications