diff --git a/doc/source/V2_00.rst b/doc/source/V2_00.rst index 30437f0f0fa9ae2f664be51f338ab7e7250be469..3d2640bc643166709e4fa1baf1e8c535a36e5c4c 100644 --- a/doc/source/V2_00.rst +++ b/doc/source/V2_00.rst @@ -179,7 +179,7 @@ files (.json and .py). ***MQTT interface*** -Interface to communicate with the Pi designed for the Internet of Things (IoT). This interface enable to control a network of OhmPi remotely through an MQTT broker. An example of MQTT broker that can be used is `Mosquitto <https://mosquitto.org/>`_. Commands are received by ohmpi.py script and processed. All commands are sent in JSON format following the Python API with args and kwargs: +Interface to communicate with the Pi designed for the Internet of Things (IoT). This interface allows to control a single OhmPi, a network of OhmPis, or auxilliary instruments remotely through an MQTT broker. An example of MQTT broker that can be used is `Mosquitto <https://mosquitto.org/>`_. Depending on the experiment needs, MQTT brokers can be set up locally on the Raspberry Pi, on a master Raspberry Pi or on a local or remote server. Commands are received by ohmpi.py script via the mqtt_interface.py script and further processed. All commands are sent in JSON format following the Python API with args and kwargs: .. code-block:: json :caption: Updating acquisition settings. @@ -206,7 +206,15 @@ Interface to communicate with the Pi designed for the Internet of Things (IoT). "cmd_id": "3fzxv121UITwGjWYgcz4xw", "cmd": "rs_check", } - + +.. code-block:: json + :caption: Running a sequence. + + { + "cmd_id": "3fzxv121UITwGjWYgcz4Yw", + "cmd": "run_sequence", + } + .. code-block:: json :caption: Running same sequence multiple times (nb_meas). @@ -220,11 +228,10 @@ Interface to communicate with the Pi designed for the Internet of Things (IoT). { "cmd_id": "3fzxv121UITwGjWYgcz4xw", - "cmd": "update_settings", + "cmd": "interrupt", } - diff --git a/ohmpi.py b/ohmpi.py index 70c50dcc0f02608bcffb0eee5fe00eeb587c6d96..d22ab1fce84471a6364c92f0553b17b29bbd12dd 100644 --- a/ohmpi.py +++ b/ohmpi.py @@ -195,11 +195,6 @@ class OhmPi(object): def on_message(client, userdata, message): command = message.payload.decode('utf-8') self.exec_logger.debug(f'Received command {command}') - # dic = json.loads(command) - # if dic['cmd_id'] != self.cmd_id: - # self.cmd_id = dic['cmd_id'] - # payload = json.dumps({'cmd_id': dic['cmd_id'], 'reply': 'ok'}) - # publish.single(payload=payload, **publisher_config) self._process_commands(command) self.controller.on_message = on_message @@ -388,12 +383,12 @@ class OhmPi(object): def _find_identical_in_line(quads): """Finds quadrupole where A and B are identical. If A and B are connected to the same electrode, the Pi burns (short-circuit). - + Parameters ---------- quads : numpy.ndarray List of quadrupoles of shape nquad x 4 or 1D vector of shape nquad. - + Returns ------- output : numpy.ndarray 1D array of int @@ -625,7 +620,7 @@ class OhmPi(object): """ self.exec_logger.debug('Starting measurement') - self.exec_logger.info('Waiting for data') # do we need this as info? debug is enough I think (gb) + self.exec_logger.debug('Waiting for data') # check arguments if quad is None: @@ -920,6 +915,121 @@ class OhmPi(object): self.thread = threading.Thread(target=func) self.thread.start() + def run_sequence(self, cmd_id=None, **kwargs): + """Runs sequence synchronously (=blocking on main thread). + Additional arguments are passed to run_measurement(). + """ + self.status = 'running' + self.exec_logger.debug(f'Status: {self.status}') + self.exec_logger.debug(f'Measuring sequence: {self.sequence}') + t0 = time.time() + + # create filename with timestamp + filename = self.settings["export_path"].replace('.csv', + f'_{datetime.now().strftime("%Y%m%dT%H%M%S")}.csv') + self.exec_logger.debug(f'Saving to {filename}') + + # make sure all multiplexer are off + self.reset_mux() + + # measure all quadrupole of the sequence + if self.sequence is None: + n = 1 + else: + n = self.sequence.shape[0] + for i in range(0, n): + if self.sequence is None: + quad = np.array([0, 0, 0, 0]) + else: + quad = self.sequence[i, :] # quadrupole + if self.status == 'stopping': + break + + # call the switch_mux function to switch to the right electrodes + self.switch_mux_on(quad) + + # run a measurement + if self.on_pi: + acquired_data = self.run_measurement(quad, **kwargs) + else: # for testing, generate random data + acquired_data = { + 'A': [quad[0]], 'B': [quad[1]], 'M': [quad[2]], 'N': [quad[3]], + 'R [ohm]': np.abs(np.random.randn(1)) + } + + # switch mux off + self.switch_mux_off(quad) + + # add command_id in dataset + acquired_data.update({'cmd_id': cmd_id}) + # log data to the data logger + # self.data_logger.info(f'{acquired_data}') + # save data and print in a text file + self.append_and_save(filename, acquired_data) + self.exec_logger.debug(f'quadrupole {i + 1:d}/{n:d}') + + self.status = 'idle' + + def run_sequence_async(self, cmd_id=None, **kwargs): + """Runs the sequence in a separate thread. Can be stopped by 'OhmPi.interrupt()'. + Additional arguments are passed to run_measurement(). + + Parameters + ---------- + cmd_id: + """ + + def func(): + self.run_sequence(**kwargs) + + self.thread = threading.Thread(target=func) + self.thread.start() + self.status = 'idle' + + def run_multiple_sequences(self, cmd_id=None, sequence_delay=None, nb_meas=None, **kwargs): + """Runs multiple sequences in a separate thread for monitoring mode. + Can be stopped by 'OhmPi.interrupt()'. + Additional arguments are passed to run_measurement(). + + Parameters + ---------- + cmd_id : + + sequence_delay : int, optional + Number of seconds at which the sequence must be started from each others. + nb_meas : int, optional + Number of time the sequence must be repeated. + kwargs : dict, optional + See help(k.run_measurement) for more info. + """ + # self.run = True + if sequence_delay is None: + sequence_delay = self.settings['sequence_delay'] + sequence_delay = int(sequence_delay) + if nb_meas is None: + nb_meas = self.settings['nb_meas'] + self.status = 'running' + self.exec_logger.debug(f'Status: {self.status}') + self.exec_logger.debug(f'Measuring sequence: {self.sequence}') + + def func(): + for g in range(0, nb_meas): # for time-lapse monitoring + if self.status == 'stopping': + self.exec_logger.warning('Data acquisition interrupted') + break + t0 = time.time() + self.run_sequence(**kwargs) + + # sleeping time between sequence + dt = sequence_delay - (time.time() - t0) + if dt < 0: + dt = 0 + if nb_meas > 1: + time.sleep(dt) # waiting for next measurement (time-lapse) + self.status = 'idle' + self.thread = threading.Thread(target=func) + self.thread.start() + def run_sequence(self, cmd_id=None, **kwargs): """Runs sequence synchronously (=blocking on main thread). Additional arguments are passed to run_measurement(). @@ -1064,6 +1174,91 @@ class OhmPi(object): # # TODO if interrupted, we would need to restore the values # # TODO or we offer the possibility in 'run_measurement' to have rs_check each time? + def set_sequence(self, sequence=None): + try: + self.sequence = np.loadtxt(StringIO(sequence)).astype('uint32') + status = True + except Exception as e: + self.exec_logger.warning(f'Unable to set sequence: {e}') + status = False + + def stop(self): + warnings.warn('This function is deprecated. Use interrupt instead.', DeprecationWarning) + self.interrupt() + + def _switch_mux(self, electrode_nr, state, role): + """Selects the right channel for the multiplexer cascade for a given electrode. + + Parameters + ---------- + electrode_nr : int + Electrode index to be switched on or off. + state : str + Either 'on' or 'off'. + role : str + Either 'A', 'B', 'M' or 'N', so we can assign it to a MUX board. + """ + if not self.use_mux or not self.on_pi: + if not self.on_pi: + self.exec_logger.warning('Cannot reset mux while in simulation mode...') + else: + self.exec_logger.warning('You cannot use the multiplexer because use_mux is set to False.' + ' Set use_mux to True to use the multiplexer...') + elif self.sequence is None: + self.exec_logger.warning('Unable to switch MUX without a sequence') + else: + # choose with MUX board + tca = adafruit_tca9548a.TCA9548A(self.i2c, self.board_addresses[role]) + + # find I2C address of the electrode and corresponding relay + # considering that one MCP23017 can cover 16 electrodes + i2c_address = 7 - (electrode_nr - 1) // 16 # quotient without rest of the division + relay_nr = electrode_nr - (electrode_nr // 16) * 16 + 1 + + if i2c_address is not None: + # select the MCP23017 of the selected MUX board + mcp2 = MCP23017(tca[i2c_address]) + mcp2.get_pin(relay_nr - 1).direction = digitalio.Direction.OUTPUT + + if state == 'on': + mcp2.get_pin(relay_nr - 1).value = True + else: + mcp2.get_pin(relay_nr - 1).value = False + + self.exec_logger.debug(f'Switching relay {relay_nr} ' + f'({str(hex(self.board_addresses[role]))}) {state} for electrode {electrode_nr}') + else: + self.exec_logger.warning(f'Unable to address electrode nr {electrode_nr}') + + def switch_mux_on(self, quadrupole): + """Switches on multiplexer relays for given quadrupole. + + Parameters + ---------- + quadrupole : list of 4 int + List of 4 integers representing the electrode numbers. + """ + roles = ['A', 'B', 'M', 'N'] + # another check to be sure A != B + if quadrupole[0] != quadrupole[1]: + for i in range(0, 4): + if quadrupole[i] > 0: + self._switch_mux(quadrupole[i], 'on', roles[i]) + else: + self.exec_logger.error('Not switching MUX : A == B -> short circuit risk detected!') + + def switch_mux_off(self, quadrupole): + """Switches off multiplexer relays for given quadrupole. + + Parameters + ---------- + quadrupole : list of 4 int + List of 4 integers representing the electrode numbers. + """ + roles = ['A', 'B', 'M', 'N'] + for i in range(0, 4): + if quadrupole[i] > 0: + self._switch_mux(quadrupole[i], 'off', roles[i]) def set_sequence(self, sequence=None): try: self.sequence = np.array(sequence).astype(int) @@ -1210,6 +1405,65 @@ class OhmPi(object): assert isinstance(self._sequence, np.ndarray) return self._sequence + def reset_mux(self): + """Switches off all multiplexer relays.""" + if self.on_pi and self.use_mux: + roles = ['A', 'B', 'M', 'N'] + for i in range(0, 4): + for j in range(1, self.max_elec + 1): + self._switch_mux(j, 'off', roles[i]) + self.exec_logger.debug('All MUX switched off.') + elif not self.on_pi: + self.exec_logger.warning('Cannot reset mux while in simulation mode...') + else: + self.exec_logger.warning('You cannot use the multiplexer because use_mux is set to False.' + ' Set use_mux to True to use the multiplexer...') + + def _update_acquisition_settings(self, config): + warnings.warn('This function is deprecated, use update_settings() instead.', DeprecationWarning) + self.update_settings(config) + + def update_settings(self, config): + """Updates acquisition settings from a json file or dictionary. + Parameters can be: + - nb_electrodes (number of electrode used, if 4, no MUX needed) + - injection_duration (in seconds) + - nb_meas (total number of times the sequence will be run) + - sequence_delay (delay in second between each sequence run) + - nb_stack (number of stack for each quadrupole measurement) + - export_path (path where to export the data, timestamp will be added to filename) + + Parameters + ---------- + config : str, dict + Path to the .json settings file or dictionary of settings. + """ + status = False + if config is not None: + try: + if isinstance(config, dict): + self.settings.update(config) + else: + with open(config) as json_file: + dic = json.load(json_file) + self.settings.update(dic) + self.exec_logger.debug('Acquisition parameters updated: ' + str(self.settings)) + status = True + except Exception as e: + self.exec_logger.warning('Unable to update settings.') + status = False + else: + self.exec_logger.warning('Settings are missing...') + return status + + # Properties + @property + def sequence(self): + """Gets sequence""" + if self._sequence is not None: + assert isinstance(self._sequence, np.ndarray) + return self._sequence + @sequence.setter def sequence(self, sequence): """Sets sequence"""