From eeb82292726a3cc164b66db44782c1a20ff137a6 Mon Sep 17 00:00:00 2001
From: su530201 <olivier.kaufmann@umons.ac.be>
Date: Sun, 30 Oct 2022 18:18:20 +0100
Subject: [PATCH] Implements the generic _process_command method. Cleans up the
 code following PEP.

---
 compressed_sized_timed_rotating_logger.py |  20 +-
 config.py                                 |  10 +-
 logging_setup.py                          |  39 +--
 ohmpi.py                                  | 321 ++++++++++++----------
 requirements.txt                          |   7 +-
 requirements_not_on_pi.txt                |   1 -
 6 files changed, 219 insertions(+), 179 deletions(-)

diff --git a/compressed_sized_timed_rotating_logger.py b/compressed_sized_timed_rotating_logger.py
index 16c76584..308a209a 100644
--- a/compressed_sized_timed_rotating_logger.py
+++ b/compressed_sized_timed_rotating_logger.py
@@ -5,11 +5,10 @@ import zipfile
 
 
 class CompressedSizedTimedRotatingFileHandler(handlers.TimedRotatingFileHandler):
-    """
-    Handler for logging to a set of files, which switches from one file
+    """Handler for logging to a set of files, which switches from one file
     to the next when the current file reaches a certain size, or at certain
-    timed intervals
-    """
+    timed intervals"""
+
     def __init__(self, filename, max_bytes=0, backup_count=0, encoding=None,
                  delay=0, when='h', interval=1, utc=False, zip_mode=zipfile.ZIP_DEFLATED):
         handlers.TimedRotatingFileHandler.__init__(self, filename=filename, when=when, interval=interval, utc=utc,
@@ -18,11 +17,10 @@ class CompressedSizedTimedRotatingFileHandler(handlers.TimedRotatingFileHandler)
         self.zip_mode = zip_mode
 
     def shouldRollover(self, record):
-        """
-        Determine if rollover should occur.
-        Basically, see if the supplied record would cause the file to exceed
-        the size limit we have.
-        """
+        """Determines if rollover should occur.
+        Basically, sees if the supplied record would cause the file to exceed
+        the size limit we have."""
+
         if self.stream is None:                 # delay was set...
             self.stream = self._open()
         if self.maxBytes > 0:                   # are we rolling over?
@@ -36,6 +34,8 @@ class CompressedSizedTimedRotatingFileHandler(handlers.TimedRotatingFileHandler)
         return False
 
     def find_last_rotated_file(self):
+        """Looks for the last rotated file and returns it"""
+
         dir_name, base_name = os.path.split(self.baseFilename)
         file_names = os.listdir(dir_name)
         result = []
@@ -47,6 +47,8 @@ class CompressedSizedTimedRotatingFileHandler(handlers.TimedRotatingFileHandler)
         return os.path.join(dir_name, result[0])
 
     def doRollover(self):
+        """Does the roll-over by compressing the current file then deleting the uncompressed file"""
+
         super(CompressedSizedTimedRotatingFileHandler, self).doRollover()
 
         dfn = self.find_last_rotated_file()
diff --git a/config.py b/config.py
index 9345d1d9..0767b2b4 100644
--- a/config.py
+++ b/config.py
@@ -2,8 +2,8 @@ import logging
 
 from paho.mqtt.client import MQTTv31
 
-mqtt_broker = 'localhost'
-logging_suffix = '_interactive'
+mqtt_broker = 'mg3d-dev.umons.ac.be' # 'localhost'
+logging_suffix = ''
 # OhmPi configuration
 OHMPI_CONFIG = {
     'id': '0001',  # Unique identifier of the OhmPi board (string)
@@ -21,13 +21,9 @@ OHMPI_CONFIG = {
     'board_version': '22.10'
 }  # TODO: add a dictionary with INA models and associated gain values
 
-# CONTROL_CONFIG = {
-#     'tcp_port': 5555,
-#     'interface': 'mqtt_interface.py'  # 'http_interface'
-# }
 # Execution logging configuration
 EXEC_LOGGING_CONFIG = {
-    'logging_level': logging.DEBUG,
+    'logging_level': logging.INFO,
     'logging_to_console': True,
     'file_name': f'exec{logging_suffix}.log',
     'max_bytes': 262144,
diff --git a/logging_setup.py b/logging_setup.py
index 1cc4a9c4..ad458f3d 100644
--- a/logging_setup.py
+++ b/logging_setup.py
@@ -6,9 +6,11 @@ import logging
 from mqtt_logger import MQTTHandler
 from compressed_sized_timed_rotating_logger import CompressedSizedTimedRotatingFileHandler
 import sys
+from termcolor import colored
 
 
 def setup_loggers(mqtt=True):
+    msg = ''
     # Message logging setup
     log_path = path.join(path.dirname(__file__), 'logs')
     if not path.isdir(log_path):
@@ -37,13 +39,12 @@ def setup_loggers(mqtt=True):
                                                            interval=EXEC_LOGGING_CONFIG['interval'])
     exec_formatter = logging.Formatter(log_format)
     exec_formatter.converter = gmtime
-    exec_formatter.datefmt = '%Y/%m/%d %H:%M:%S UTC'
+    exec_formatter.datefmt = '%Y-%m-%d %H:%M:%S UTC'
     exec_handler.setFormatter(exec_formatter)
     exec_logger.addHandler(exec_handler)
     exec_logger.setLevel(EXEC_LOGGING_CONFIG['logging_level'])
 
     if logging_to_console:
-        print(f'logging exec ? {logging_to_console}') # TODO: delete this line
         console_exec_handler = logging.StreamHandler(sys.stdout)
         console_exec_handler.setLevel(EXEC_LOGGING_CONFIG['logging_level'])
         console_exec_handler.setFormatter(exec_formatter)
@@ -52,14 +53,16 @@ def setup_loggers(mqtt=True):
     if mqtt:
         mqtt_settings = MQTT_LOGGING_CONFIG.copy()
         [mqtt_settings.pop(i) for i in ['client_id', 'exec_topic', 'data_topic', 'soh_topic']]
-        mqtt_settings.update({'topic':MQTT_LOGGING_CONFIG['exec_topic']})
+        mqtt_settings.update({'topic': MQTT_LOGGING_CONFIG['exec_topic']})
         # TODO: handle the case of MQTT broker down or temporarily unavailable
         try:
             mqtt_exec_handler = MQTTHandler(**mqtt_settings)
             mqtt_exec_handler.setLevel(EXEC_LOGGING_CONFIG['logging_level'])
             mqtt_exec_handler.setFormatter(exec_formatter)
             exec_logger.addHandler(mqtt_exec_handler)
-        except:
+            msg+=colored(f"\n\u2611 Publishes execution as {MQTT_LOGGING_CONFIG['exec_topic']} topic on the {MQTT_LOGGING_CONFIG['hostname']} broker", 'blue')
+        except Exception as e:
+            msg += colored(f'\nWarning: Unable to connect to exec topic on broker\n{e}', 'yellow')
             mqtt = False
 
     # Set data logging format and level
@@ -88,17 +91,22 @@ def setup_loggers(mqtt=True):
         mqtt_settings = MQTT_LOGGING_CONFIG.copy()
         [mqtt_settings.pop(i) for i in ['client_id', 'exec_topic', 'data_topic', 'soh_topic']]
         mqtt_settings.update({'topic': MQTT_LOGGING_CONFIG['data_topic']})
-        mqtt_data_handler = MQTTHandler(**mqtt_settings)
-        mqtt_data_handler.setLevel(DATA_LOGGING_CONFIG['logging_level'])
-        mqtt_data_handler.setFormatter(data_formatter)
-        data_logger.addHandler(mqtt_data_handler)
+        try:
+            mqtt_data_handler = MQTTHandler(**mqtt_settings)
+            mqtt_data_handler.setLevel(DATA_LOGGING_CONFIG['logging_level'])
+            mqtt_data_handler.setFormatter(data_formatter)
+            data_logger.addHandler(mqtt_data_handler)
+            msg += colored(f"\n\u2611 Publishes data as {MQTT_LOGGING_CONFIG['data_topic']} topic on the {MQTT_LOGGING_CONFIG['hostname']} broker", 'blue')
+        except Exception as e:
+            msg += colored(f'\nWarning: Unable to connect to data topic on broker\n{e}', 'yellow')
+            mqtt = False
 
     try:
         init_logging(exec_logger, data_logger, EXEC_LOGGING_CONFIG['logging_level'], log_path, data_log_filename)
     except Exception as err:
-        print(f'ERROR: Could not initialize logging!\n{err}')
+        msg += colored(f'\n\u26A0 ERROR: Could not initialize logging!\n{err}', 'red')
     finally:
-        return exec_logger, exec_log_filename, data_logger, data_log_filename, EXEC_LOGGING_CONFIG['logging_level']
+        return exec_logger, exec_log_filename, data_logger, data_log_filename, EXEC_LOGGING_CONFIG['logging_level'], msg
 
 
 def init_logging(exec_logger, data_logger, exec_logging_level, log_path, data_log_filename):
@@ -110,7 +118,7 @@ def init_logging(exec_logger, data_logger, exec_logging_level, log_path, data_lo
     exec_logger.info('*** NEW SESSION STARTING ***')
     exec_logger.info('****************************')
     exec_logger.info('')
-    exec_logger.info('Logging level: %s' % exec_logging_level)
+    exec_logger.debug('Logging level: %s' % exec_logging_level)
     try:
         st = statvfs('.')
         available_space = st.f_bavail * st.f_frsize / 1024 / 1024
@@ -118,15 +126,14 @@ def init_logging(exec_logger, data_logger, exec_logging_level, log_path, data_lo
     except Exception as e:
         exec_logger.debug('Unable to get remaining disk space: {e}')
     exec_logger.info('Saving data log to ' + data_log_filename)
-    exec_logger.info('OhmPi settings:')
-    # TODO Add OhmPi settings
     config_dict = {'execution logging configuration': json.dumps(EXEC_LOGGING_CONFIG, indent=4),
                    'data logging configuration': json.dumps(DATA_LOGGING_CONFIG, indent=4),
                    'mqtt logging configuration': json.dumps(MQTT_LOGGING_CONFIG, indent=4),
                    'mqtt control configuration': json.dumps(MQTT_CONTROL_CONFIG, indent=4)}
     for k, v in config_dict.items():
-        exec_logger.info(f'{k}:\n{v}')
-    exec_logger.info('')
-    exec_logger.info(f'init_logging_status: {init_logging_status}')
+        exec_logger.debug(f'{k}:\n{v}')
+    exec_logger.debug('')
+    if not init_logging_status:
+        exec_logger.warning(f'Logging initialisation has encountered a problem.')
     data_logger.info('Starting_session')
     return init_logging_status
diff --git a/ohmpi.py b/ohmpi.py
index 294a5347..1eb11007 100644
--- a/ohmpi.py
+++ b/ohmpi.py
@@ -3,7 +3,7 @@
 created on January 6, 2020.
 Updates May 2022, Oct 2022.
 Ohmpi.py is a program to control a low-cost and open hardware resistivity meter OhmPi that has been developed by
-Rémi CLEMENT (INRAE),Vivien DUBOIS (INRAE), Hélène GUYARD (IGE), Nicolas FORQUET (INRAE), Yannick FARGIER (IFSTTAR)
+Rémi CLEMENT (INRAE), Vivien DUBOIS (INRAE), Hélène GUYARD (IGE), Nicolas FORQUET (INRAE), Yannick FARGIER (IFSTTAR)
 Olivier KAUFMANN (UMONS), Arnaud WATELET (UMONS) and Guillaume BLANCHY (ILVO).
 """
 
@@ -21,7 +21,8 @@ from termcolor import colored
 import threading
 import paho.mqtt.client as mqtt_client
 from logging_setup import setup_loggers
-from config import MQTT_CONTROL_CONFIG, OHMPI_CONFIG
+from config import MQTT_CONTROL_CONFIG, OHMPI_CONFIG, EXEC_LOGGING_CONFIG
+from logging import DEBUG
 
 # finish import (done only when class is instantiated as some libs are only available on arm64 platform)
 try:
@@ -36,10 +37,10 @@ try:
     from digitalio import Direction  # noqa
     from gpiozero import CPUTemperature  # noqa
     import minimalmodbus
-
     arm64_imports = True
 except ImportError as error:
-    print(colored(f'Import error: {error}', 'yellow'))
+    if EXEC_LOGGING_CONFIG['logging_level'] == DEBUG:
+        print(colored(f'Import error: {error}', 'yellow'))
     arm64_imports = False
 except Exception as error:
     print(colored(f'Unexpected error: {error}', 'red'))
@@ -47,19 +48,28 @@ except Exception as error:
 
 
 class OhmPi(object):
-    """Create the main OhmPi object.
-
-    Parameters
-    ----------
-    settings : str, optional
-        Path to the .json configuration file.
-    sequence : str, optional
-        Path to the .txt where the sequence is read. By default, a 1 quadrupole
-        sequence: 1, 2, 3, 4 is used.
+    """ OhmPi class.
     """
 
     def __init__(self, settings=None, sequence=None, use_mux=False, mqtt=True, on_pi=None, idps=False):
-        # flags and attributes
+        """Constructs the ohmpi object
+         
+        Parameters
+        ----------
+        settings:
+
+        sequence:
+
+        use_mux:
+            if True use the multiplexor to select active electrodes
+        mqtt: bool, defaut: True
+            if True publish on mqtt topics while logging, otherwise use other loggers only
+        on_pi: bool,None default: None
+            if None, the platform on which the class is instanciated is determined to set on_pi to either True or False
+        idps:
+            if true uses the DPS
+        """
+        
         if on_pi is None:
             _, on_pi = OhmPi._get_platform()
 
@@ -70,21 +80,18 @@ class OhmPi(object):
         self.thread = None  # contains the handle for the thread taking the measurement
 
         # set loggers
-        config_exec_logger, _, config_data_logger, _, _ = setup_loggers(mqtt=mqtt)  # TODO: add SOH
+        config_exec_logger, _, config_data_logger, _, _, msg = setup_loggers(mqtt=mqtt)  # TODO: add SOH
         self.data_logger = config_data_logger
         self.exec_logger = config_exec_logger
-        self.soh_logger = None
-        print('Loggers:')
-        print(colored(f'Exec logger {self.exec_logger.handlers if self.exec_logger is not None else "None"}', 'blue'))
-        print(colored(f'Data logger {self.data_logger.handlers if self.data_logger is not None else "None"}', 'blue'))
-        print(colored(f'SOH logger {self.soh_logger.handlers if self.soh_logger is not None else "None"}', 'blue'))
+        self.soh_logger = None  # TODO: Implement the SOH logger
+        print(msg)
 
         # set controller
-        self.controller = mqtt_client.Client(f"ohmpi_{OHMPI_CONFIG['id']}_listener", clean_session=False)  # create new instance
-        print(colored(f"Connecting to control topic {MQTT_CONTROL_CONFIG['ctrl_topic']} on {MQTT_CONTROL_CONFIG['hostname']} broker", 'blue'))
+        self.controller = mqtt_client.Client(f"ohmpi_{OHMPI_CONFIG['id']}_listener", clean_session=False)
         trials = 0
         trials_max = 10
         broker_connected = False
+        self.exec_logger.debug(f"Connecting to control broker: {MQTT_CONTROL_CONFIG['hostname']}")
         while trials < trials_max:
             try:
                 self.controller.username_pw_set(MQTT_CONTROL_CONFIG['auth'].get('username'),
@@ -94,12 +101,21 @@ class OhmPi(object):
                 broker_connected = True
             except Exception as e:
                 self.exec_logger.debug(f'Unable to connect control broker: {e}')
-                self.exec_logger.info('trying again to connect to control broker...')
+                self.exec_logger.debug('Retrying to connect control broker...')
                 time.sleep(2)
                 trials += 1
         if broker_connected:
-            self.exec_logger.info(f"Subscribing to control topic {MQTT_CONTROL_CONFIG['ctrl_topic']}")
-            self.controller.subscribe(MQTT_CONTROL_CONFIG['ctrl_topic'], MQTT_CONTROL_CONFIG['qos'])
+            self.exec_logger.debug(f"Subscribing to control topic {MQTT_CONTROL_CONFIG['ctrl_topic']}")
+            try:
+                self.controller.subscribe(MQTT_CONTROL_CONFIG['ctrl_topic'], MQTT_CONTROL_CONFIG['qos'])
+
+                msg = f"\u2611 Subscribed to control topic {MQTT_CONTROL_CONFIG['ctrl_topic']}" \
+                      f" on {MQTT_CONTROL_CONFIG['hostname']} broker"
+                self.exec_logger.debug(msg)
+                print(colored(msg, 'blue'))
+            except Exception as e:
+                self.exec_logger.warning(f'Unable to subscribe to control topic : {e}')
+                self.controller = None
         else:
             self.exec_logger.error(f"Unable to connect to control broker on {MQTT_CONTROL_CONFIG['hostname']}")
             self.controller = None
@@ -119,7 +135,6 @@ class OhmPi(object):
         if settings is not None:
             self.update_settings(settings)
 
-        print(self.settings)
         self.exec_logger.debug('Initialized with settings:' + str(self.settings))
 
         # read quadrupole sequence
@@ -146,13 +161,13 @@ class OhmPi(object):
 
             # current injection module
             if self.idps:
-                self.DPS = minimalmodbus.Instrument(port='/dev/ttyUSB0', slaveaddress=1) # port name, slave address (in decimal)
+                self.DPS = minimalmodbus.Instrument(port='/dev/ttyUSB0', slaveaddress=1)  # port name, address (decimal)
                 self.DPS.serial.baudrate = 9600                      # Baud rate 9600 as listed in doc
                 self.DPS.serial.bytesize = 8                         #
-                self.DPS.serial.timeout  = 1                         # greater than 0.5 for it to work
-                self.DPS.debug           = False                     #
-                self.DPS.serial.parity   = 'N'                       # No parity
-                self.DPS.mode            = minimalmodbus.MODE_RTU    # RTU mode
+                self.DPS.serial.timeout = 1                         # greater than 0.5 for it to work
+                self.DPS.debug = False                               #
+                self.DPS.serial.parity = 'N'                       # No parity
+                self.DPS.mode = minimalmodbus.MODE_RTU    # RTU mode
                 self.DPS.write_register(0x0001, 40, 0)   # max current allowed (36 mA for relays)
                 # (last number) 0 is for mA, 3 is for A
 
@@ -165,9 +180,13 @@ class OhmPi(object):
             self.pin1.value = False
 
         # Starts the command processing thread
-        self.cmd_listen = True
-        self.cmd_thread = threading.Thread(target=self._control)
-        self.cmd_thread.start()
+        if self.controller is not None:
+            self.cmd_listen = True
+            self.cmd_thread = threading.Thread(target=self._control)
+            self.cmd_thread.start()
+        else:
+            self.exec_logger.warning('No connection to control broker.'
+                                     ' Use python/ipython to interact with OhmPi object...')
 
     @property
     def sequence(self):
@@ -187,6 +206,7 @@ class OhmPi(object):
         self._sequence = sequence
 
     def _control(self):
+        """Gets commands from the controller(s) and execute them"""
         def on_message(client, userdata, message):
             command = message.payload.decode('utf-8')
             self.exec_logger.debug(f'Received command {command}')
@@ -195,7 +215,7 @@ class OhmPi(object):
         self.controller.on_message = on_message
         self.controller.loop_start()
         while True:
-            time.sleep(.5)
+            time.sleep(.5)  # TODO: Check if this waiting time should be reduced...
 
     def _update_acquisition_settings(self, config):
         warnings.warn('This function is deprecated, use update_settings() instead.', DeprecationWarning)
@@ -213,8 +233,8 @@ class OhmPi(object):
 
         Parameters
         ----------
-        config : str
-            Path to the .json or dictionary.
+        config : str, dict
+            Path to the .json settings file or dictionary of settings.
         """
         status = False
         if config is not None:
@@ -241,7 +261,7 @@ class OhmPi(object):
         self.id = OHMPI_CONFIG['id']  # ID of the OhmPi
         self.r_shunt = OHMPI_CONFIG['R_shunt']  # reference resistance value in ohm
         self.Imax = OHMPI_CONFIG['Imax']  # maximum current
-        self.exec_logger.warning(f'The maximum current cannot be higher than {self.Imax} mA')
+        self.exec_logger.debug(f'The maximum current cannot be higher than {self.Imax} mA')
         self.coef_p2 = OHMPI_CONFIG['coef_p2']  # slope for current conversion for ads.P2, measurement in V/V
         self.nb_samples = OHMPI_CONFIG['integer']  # number of samples measured for each stack
         self.version = OHMPI_CONFIG['version']  # hardware version
@@ -265,7 +285,6 @@ class OhmPi(object):
         output : numpy.ndarray 1D array of int
             List of index of rows where A and B are identical.
         """
-        # TODO is this needed for M and N?
 
         # if we have a 1D array (so only 1 quadrupole), make it a 2D array
         if len(quads.shape) == 1:
@@ -278,8 +297,9 @@ class OhmPi(object):
     @staticmethod
     def _get_platform():
         """Gets platform name and checks if it is a raspberry pi
+        
         Returns
-        =======
+        -------
         str, bool
             name of the platform on which the code is running, boolean that is true if the platform is a raspberry pi"""
 
@@ -296,10 +316,10 @@ class OhmPi(object):
 
     def read_quad(self, filename):
         warnings.warn('This function is deprecated. Use load_sequence instead.', DeprecationWarning)
-        self.load_sequence(self, filename)
+        self.load_sequence(filename)
 
-    def load_sequence(self, filename):
-        """Read quadrupole sequence from file.
+    def load_sequence(self, filename: str):
+        """Reads quadrupole sequence from file.
 
         Parameters
         ----------
@@ -312,10 +332,11 @@ class OhmPi(object):
         sequence : numpy.array
             Array of shape (number quadrupoles * 4).
         """
+        self.exec_logger.debug(f'Loading sequence {filename}')
         sequence = np.loadtxt(filename, delimiter=" ", dtype=np.uint32)  # load quadrupole file
 
         if sequence is not None:
-            self.exec_logger.debug('Sequence of {:d} quadrupoles read.'.format(sequence.shape[0]))
+            self.exec_logger.debug(f'Sequence of {sequence.shape[0]:d} quadrupoles read.')
 
         # locate lines where the electrode index exceeds the maximum number of electrodes
         test_index_elec = np.array(np.where(sequence > self.max_elec))
@@ -337,14 +358,14 @@ class OhmPi(object):
             sequence = None
 
         if sequence is not None:
-            self.exec_logger.info('Sequence of {:d} quadrupoles read.'.format(sequence.shape[0]))
+            self.exec_logger.info(f'Sequence {filename} of {sequence.shape[0]:d} quadrupoles loaded.')
         else:
             self.exec_logger.warning(f'Unable to load sequence {filename}')
 
         self.sequence = sequence
 
     def _switch_mux(self, electrode_nr, state, role):
-        """Select the right channel for the multiplexer cascade for a given electrode.
+        """Selects the right channel for the multiplexer cascade for a given electrode.
         
         Parameters
         ----------
@@ -355,8 +376,12 @@ class OhmPi(object):
         role : str
             Either 'A', 'B', 'M' or 'N', so we can assign it to a MUX board.
         """
-        if not self.use_mux:
-            pass # no MUX or don't use MUX
+        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:
@@ -366,7 +391,7 @@ class OhmPi(object):
             # 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
+            relay_nr = electrode_nr - (electrode_nr // 16) * 16 + 1
 
             if i2c_address is not None:
                 # select the MCP23017 of the selected MUX board
@@ -382,9 +407,8 @@ class OhmPi(object):
             else:
                 self.exec_logger.warning(f'Unable to address electrode nr {electrode_nr}')
 
-
     def switch_mux_on(self, quadrupole):
-        """ Switch on multiplexer relays for given quadrupole.
+        """Switches on multiplexer relays for given quadrupole.
         
         Parameters
         ----------
@@ -401,7 +425,7 @@ class OhmPi(object):
             self.exec_logger.error('A == B -> short circuit risk detected!')
 
     def switch_mux_off(self, quadrupole):
-        """ Switch off multiplexer relays for given quadrupole.
+        """Switches off multiplexer relays for given quadrupole.
         
         Parameters
         ----------
@@ -414,15 +438,21 @@ class OhmPi(object):
                 self._switch_mux(quadrupole[i], 'off', roles[i])
 
     def reset_mux(self):
-        """Switch off all multiplexer relays."""
-        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.')
+        """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 _gain_auto(self, channel):
-        """ Automatically set the gain on a channel
+        """Automatically sets the gain on a channel
 
         Parameters
         ----------
@@ -445,7 +475,7 @@ class OhmPi(object):
         return gain
 
     def _compute_tx_volt(self, best_tx_injtime=0.1, strategy='vmax', tx_volt=5):
-        """Estimating best Tx voltage based on different strategy.
+        """Estimates best Tx voltage based on different strategies.
         At first a half-cycle is made for a short duration with a fixed
         known voltage. This gives us Iab and Rab. We also measure Vmn.
         A constant c = vmn/iab is computed (only depends on geometric
@@ -505,50 +535,50 @@ class OhmPi(object):
 
         # set voltage for test
         self.DPS.write_register(0x0000, volt, 2)
-        self.DPS.write_register(0x09, 1) # DPS5005 on
-        time.sleep(best_tx_injtime) # inject for given tx time
+        self.DPS.write_register(0x09, 1)  # DPS5005 on
+        time.sleep(best_tx_injtime)  # inject for given tx time
 
         # autogain
         self.ads_current = ads.ADS1115(self.i2c, gain=2/3, data_rate=860, address=self.ads_current_address)
         self.ads_voltage = ads.ADS1115(self.i2c, gain=2/3, data_rate=860, address=self.ads_voltage_address)
-        #print('current P0', AnalogIn(self.ads_current, ads.P0).voltage)
-        #print('voltage P0', AnalogIn(self.ads_voltage, ads.P0).voltage)
-        #print('voltage P2', AnalogIn(self.ads_voltage, ads.P2).voltage)
-        gain_current = self.gain_auto(AnalogIn(self.ads_current, ads.P0))
-        gain_voltage0 = self.gain_auto(AnalogIn(self.ads_voltage, ads.P0))
-        gain_voltage2 = self.gain_auto(AnalogIn(self.ads_voltage, ads.P2))
+        # print('current P0', AnalogIn(self.ads_current, ads.P0).voltage)
+        # print('voltage P0', AnalogIn(self.ads_voltage, ads.P0).voltage)
+        # print('voltage P2', AnalogIn(self.ads_voltage, ads.P2).voltage)
+        gain_current = self._gain_auto(AnalogIn(self.ads_current, ads.P0))
+        gain_voltage0 = self._gain_auto(AnalogIn(self.ads_voltage, ads.P0))
+        gain_voltage2 = self._gain_auto(AnalogIn(self.ads_voltage, ads.P2))
         gain_voltage = np.min([gain_voltage0, gain_voltage2])
-        #print('gain current: {:.3f}, gain voltage: {:.3f}'.format(gain_current, gain_voltage))
+        # print('gain current: {:.3f}, gain voltage: {:.3f}'.format(gain_current, gain_voltage))
         self.ads_current = ads.ADS1115(self.i2c, gain=gain_current, data_rate=860, address=self.ads_current_address)
         self.ads_voltage = ads.ADS1115(self.i2c, gain=gain_voltage, data_rate=860, address=self.ads_voltage_address)
 
         # we measure the voltage on both A0 and A2 to guess the polarity
-        I = (AnalogIn(self.ads_current, ads.P0).voltage) * 1000/50/self.r_shunt # measure current
-        U0 = AnalogIn(self.ads_voltage, ads.P0).voltage * 1000 # measure voltage
+        I = AnalogIn(self.ads_current, ads.P0).voltage * 1000/50/self.r_shunt  # measure current
+        U0 = AnalogIn(self.ads_voltage, ads.P0).voltage * 1000  # measure voltage
         U2 = AnalogIn(self.ads_voltage, ads.P2).voltage * 1000
-        #print('I (mV)', I*50*self.r_shunt)
-        #print('I (mA)', I)
-        #print('U0 (mV)', U0)
-        #print('U2 (mV)', U2)
+        # print('I (mV)', I*50*self.r_shunt)
+        # print('I (mA)', I)
+        # print('U0 (mV)', U0)
+        # print('U2 (mV)', U2)
 
         # check polarity
         polarity = 1  # by default, we guessed it right
         vmn = U0
-        if U0 < 0: # we guessed it wrong, let's use a correction factor
+        if U0 < 0:  # we guessed it wrong, let's use a correction factor
             polarity = -1
             vmn = U2
-        #print('polarity', polarity)
+        # print('polarity', polarity)
 
         # compute constant
         c = vmn / I
         Rab = (volt * 1000) / I
 
-        self.exec_logger.debug('Rab = {:.2f} Ohms'.format(Rab))
+        self.exec_logger.debug(f'Rab = {Rab:.2f} Ohms')
 
         # implement different strategy
         if strategy == 'vmax':
             vmn_max = c * current_max
-            if vmn_max < voltage_max and vmn_max > voltage_min:
+            if voltage_max > vmn_max > voltage_min:
                 vab = current_max * Rab
                 self.exec_logger.debug('target max current')
             else:
@@ -561,7 +591,7 @@ class OhmPi(object):
 
         elif strategy == 'vmin':
             vmn_min = c * current_min
-            if vmn_min > voltage_min and vmn_min < voltage_max:
+            if voltage_min < vmn_min < voltage_max:
                 vab = current_min * Rab
                 self.exec_logger.debug('target min current')
             else:
@@ -577,16 +607,15 @@ class OhmPi(object):
         else:
             vab = 5
 
-        #self.DPS.write_register(0x09, 0) # DPS5005 off
+        # self.DPS.write_register(0x09, 0) # DPS5005 off
         self.pin0.value = False
         self.pin1.value = False
 
         return vab, polarity
 
-
     def run_measurement(self, quad=None, nb_stack=None, injection_duration=None,
-                        autogain=True, strategy='constant', tx_volt=5, best_tx_injtime=0.1):
-        """Do a 4 electrode measurement and measure transfer resistance obtained.
+                        autogain=True, strategy='constant', tx_volt=5., best_tx_injtime=0.1):
+        """Measures on a quadrupole and returns transfer resistance.
 
         Parameters
         ----------
@@ -604,12 +633,12 @@ class OhmPi(object):
         strategy : str, optional
             (V3.0 only) If we search for best voltage (tx_volt == 0), we can choose
             different strategy:
-            - vmin: find lowest voltage that gives us a signal
-            - vmax: find max voltage that are in the range
+            - vmin: find the lowest voltage that gives us a signal
+            - vmax: find the highest voltage that stays in the range
             For a constant value, just set the tx_volt.
         tx_volt : float, optional
             (V3.0 only) If specified, voltage will be imposed. If 0, we will look
-            for the best voltage. If a best Tx cannot be found, no
+            for the best voltage. If the best Tx cannot be found, no
             measurement will be taken and values will be NaN.
         best_tx_injtime : float, optional
             (V3.0 only) Injection time in seconds used for finding the best voltage.
@@ -647,26 +676,28 @@ class OhmPi(object):
             if self.idps:
                 tx_volt, polarity = self._compute_tx_volt(
                     best_tx_injtime=best_tx_injtime, strategy=strategy, tx_volt=tx_volt)
-                self.exec_logger.debug('Best vab found is {:.3}V'.format(tx_volt))
+                self.exec_logger.debug(f'Best vab found is {tx_volt:.3f}V')
             else:
                 polarity = 1
 
             # first reset the gain to 2/3 before trying to find best gain (mode 0 is continuous)
-            self.ads_current = ads.ADS1115(self.i2c, gain=2 / 3, data_rate=860, address=self.ads_current_address, mode=0)
-            self.ads_voltage = ads.ADS1115(self.i2c, gain=2 / 3, data_rate=860, address=self.ads_voltage_address, mode=0)
+            self.ads_current = ads.ADS1115(self.i2c, gain=2 / 3, data_rate=860,
+                                           address=self.ads_current_address, mode=0)
+            self.ads_voltage = ads.ADS1115(self.i2c, gain=2 / 3, data_rate=860,
+                                           address=self.ads_voltage_address, mode=0)
 
             # turn on the power supply
             out_of_range = False
             if self.idps:
                 if not np.isnan(tx_volt):
-                    self.DPS.write_register(0x0000, tx_volt, 2) # set tx voltage in V
-                    self.DPS.write_register(0x09, 1) # DPS5005 on
+                    self.DPS.write_register(0x0000, tx_volt, 2)  # set tx voltage in V
+                    self.DPS.write_register(0x09, 1)  # DPS5005 on
                     time.sleep(0.05)
                 else:
                     self.exec_logger.debug('No best voltage found, will not take measurement')
-                    out_of_range = True # oor: out of range
+                    out_of_range = True
 
-            if not out_of_range:  # we found a vab in the range so we measure
+            if not out_of_range:  # we found a Vab in the range so we measure
                 if autogain:
                     # compute autogain
                     self.pin0.value = True
@@ -679,9 +710,11 @@ class OhmPi(object):
                         gain_voltage = self._gain_auto(AnalogIn(self.ads_voltage, ads.P2))
                     self.pin0.value = False
                     self.pin1.value = False
-                    self.exec_logger.debug('Gain current: {:.3f}, gain voltage: {:.3f}'.format(gain_current, gain_voltage))
-                    self.ads_current = ads.ADS1115(self.i2c, gain=gain_current, data_rate=860, address=self.ads_current_address, mode=0)
-                    self.ads_voltage = ads.ADS1115(self.i2c, gain=gain_voltage, data_rate=860, address=self.ads_voltage_address, mode=0)
+                    self.exec_logger.debug(f'Gain current: {gain_current:.3f}, gain voltage: {gain_voltage:.3f}')
+                    self.ads_current = ads.ADS1115(self.i2c, gain=gain_current, data_rate=860,
+                                                   address=self.ads_current_address, mode=0)
+                    self.ads_voltage = ads.ADS1115(self.i2c, gain=gain_voltage, data_rate=860,
+                                                   address=self.ads_voltage_address, mode=0)
 
                 self.pin0.value = False
                 self.pin1.value = False
@@ -722,15 +755,15 @@ class OhmPi(object):
                             if pinMN == 0:
                                 meas[k, 1] = AnalogIn(self.ads_voltage, ads.P0).voltage * 1000
                             else:
-                                meas[k, 1] = AnalogIn(self.ads_voltage, ads.P2).voltage * 1000 *-1
+                                meas[k, 1] = -AnalogIn(self.ads_voltage, ads.P2).voltage * 1000
                         elif self.board_version == '22.10':
                             meas[k, 1] = -AnalogIn(self.ads_voltage, ads.P0, ads.P1).voltage * self.coef_p2 * 1000
-                        #else:
+                        # else:
                         #   self.exec_logger.debug('Unknown board')
                         time.sleep(sampling_interval / 1000)
                         dt = time.time() - start_delay  # real injection time (s)
                         meas[k, 2] = time.time() - start_time
-                        if dt > (injection_duration - 0 * sampling_interval /1000):
+                        if dt > (injection_duration - 0 * sampling_interval / 1000.):
                             break
 
                     # stop current injection
@@ -747,20 +780,20 @@ class OhmPi(object):
                     dt = 0
                     for k in range(0, measpp.shape[0]):
                         # reading current value on ADS channels
-                        measpp[k, 0] = (AnalogIn(self.ads_current, ads.P0).voltage * 1000) / (50 * self.r_shunt)
+                        measpp[k, 0] = (AnalogIn(self.ads_current, ads.P0).voltage * 1000.) / (50 * self.r_shunt)
                         if self.board_version == '22.11':
                             if pinMN == 0:
-                                measpp[k, 1] = AnalogIn(self.ads_voltage, ads.P0).voltage * 1000
+                                measpp[k, 1] = AnalogIn(self.ads_voltage, ads.P0).voltage * 1000.
                             else:
-                                measpp[k, 1] = AnalogIn(self.ads_voltage, ads.P2).voltage * 1000 *-1
+                                measpp[k, 1] = AnalogIn(self.ads_voltage, ads.P2).voltage * 1000. * -1
                         elif self.board_version == '22.10':
-                            measpp[k, 1] = -AnalogIn(self.ads_voltage, ads.P0, ads.P1).voltage * self.coef_p2 * 1000
+                            measpp[k, 1] = -AnalogIn(self.ads_voltage, ads.P0, ads.P1).voltage * self.coef_p2 * 1000.
                         else:
                             self.exec_logger.debug('unknown board')
                         time.sleep(sampling_interval / 1000)
                         dt = time.time() - start_delay  # real injection time (s)
                         measpp[k, 2] = time.time() - start_time
-                        if dt > (injection_duration - 0 * sampling_interval /1000):
+                        if dt > (injection_duration - 0 * sampling_interval / 1000.):
                             break
 
                     end_delay = time.time()
@@ -802,11 +835,11 @@ class OhmPi(object):
 
             if self.idps:
                 self.DPS.write_register(0x0000, 0, 2)  # reset to 0 volt
-                self.DPS.write_register(0x09, 0) # DPS5005 off
+                self.DPS.write_register(0x09, 0)  # DPS5005 off
 
             # reshape full data to an array of good size
             # we need an array of regular size to save in the csv
-            if out_of_range == False:
+            if not out_of_range:
                 fulldata = np.vstack(fulldata)
                 # we create a big enough array given nb_samples, number of
                 # half-cycles (1 stack = 2 half-cycles), and twice as we
@@ -824,13 +857,13 @@ class OhmPi(object):
                 "B": quad[1],
                 "M": quad[2],
                 "N": quad[3],
-                "inj time [ms]": (end_delay - start_delay) * 1000 if out_of_range == False else 0,
+                "inj time [ms]": (end_delay - start_delay) * 1000. if not out_of_range else 0.,
                 "Vmn [mV]": sum_vmn / (2 * nb_stack),
                 "I [mA]": sum_i / (2 * nb_stack),
                 "R [ohm]": sum_vmn / sum_i,
                 "Ps [mV]": sum_ps / (2 * nb_stack),
                 "nbStack": nb_stack,
-                "Tx [V]": tx_volt if out_of_range == False else 0,
+                "Tx [V]": tx_volt if not out_of_range else 0.,
                 "CPU temp [degC]": CPUTemperature().temperature,
                 "Nb samples [-]": self.nb_samples,
                 "fulldata": fulldata,
@@ -863,8 +896,7 @@ class OhmPi(object):
         return d
 
     def rs_check(self, tx_volt=12):
-        """ Check contact resistance.
-        """
+        """Checks contact resistances"""
         # create custom sequence where MN == AB
         # we only check the electrodes which are in the sequence (not all might be connected)
         if self.sequence is None or not self.use_mux:
@@ -900,14 +932,14 @@ class OhmPi(object):
                 d = self.run_measurement(quad=quad, nb_stack=1, injection_duration=1, tx_volt=tx_volt, autogain=False)
 
                 if self.idps:
-                    voltage = tx_volt * 1000. # imposed voltage on dps5005
+                    voltage = tx_volt * 1000.  # imposed voltage on dps5005
                 else:
                     voltage = d['Vmn [mV]']
                 current = d['I [mA]']
 
                 # compute resistance measured (= contact resistance)
-                resist = abs(voltage / current) /1000.
-                #print(str(quad) + '> I: {:>10.3f} mA, V: {:>10.3f} mV, R: {:>10.3f} kOhm'.format(
+                resist = abs(voltage / current) / 1000.
+                # print(str(quad) + '> I: {:>10.3f} mA, V: {:>10.3f} mV, R: {:>10.3f} kOhm'.format(
                 #    current, voltage, resist))
                 msg = f'Contact resistance {str(quad):s}: I: {current * 1000.:>10.3f} mA, ' \
                           f'V: {voltage :>10.3f} mV, ' \
@@ -917,8 +949,7 @@ class OhmPi(object):
 
                 # if contact resistance = 0 -> we have a short circuit!!
                 if resist < 1e-5:
-                    msg = '!!!SHORT CIRCUIT!!! {:s}: {:.3f} kOhm'.format(
-                        str(quad), resist)
+                    msg = f'!!!SHORT CIRCUIT!!! {str(quad):s}: {resist:.3f} kOhm'
                     self.exec_logger.warning(msg)
                     print(msg)
 
@@ -943,7 +974,7 @@ class OhmPi(object):
 
     @staticmethod
     def append_and_save(filename, last_measurement):
-        """Append and save last measurement dict.
+        """Appends and saves the last measurement dict.
 
         Parameters
         ----------
@@ -957,15 +988,14 @@ class OhmPi(object):
             d = last_measurement['fulldata']
             n = d.shape[0]
             if n > 1:
-                idic = dict(zip(['i' + str(i) for i in range(n)], d[:,0]))
-                udic = dict(zip(['u' + str(i) for i in range(n)], d[:,1]))
-                tdic = dict(zip(['t' + str(i) for i in range(n)], d[:,2]))
+                idic = dict(zip(['i' + str(i) for i in range(n)], d[:, 0]))
+                udic = dict(zip(['u' + str(i) for i in range(n)], d[:, 1]))
+                tdic = dict(zip(['t' + str(i) for i in range(n)], d[:, 2]))
                 last_measurement.update(idic)
                 last_measurement.update(udic)
                 last_measurement.update(tdic)
             last_measurement.pop('fulldata')
 
-
         if os.path.isfile(filename):
             # Load data file and append data to it
             with open(filename, 'a') as f:
@@ -980,28 +1010,29 @@ class OhmPi(object):
                 w.writerow(last_measurement)
 
     def _process_commands(self, message):
-        """ TODO
+        """Processes commands received from the controller(s)
 
         Parameters
         ----------
         message : str
-            command and arguments
-
-        Returns
-        -------
+            message containing a command and arguments or keywords and arguments
         """
+
         try:
-            cmd_id = None
             decoded_message = json.loads(message)
+            print(f'decoded message: {decoded_message}')
             cmd_id = decoded_message.pop('cmd_id', None)
             cmd = decoded_message.pop('cmd', None)
             args = decoded_message.pop('args', '[]')
+            if args[0] != '[':
+                args = f'["{args}"]'
             args = json.loads(args)
             kwargs = decoded_message.pop('kwargs', '{}')
             kwargs = json.loads(kwargs)
             self.exec_logger.debug(f'Calling method {cmd}({args}, {kwargs})')
             status = False
-            e = None  # NOTE: Why this?
+            # e = None  # NOTE: Why this?
+            print(cmd, args, kwargs)
             if cmd_id is None:
                 self.exec_logger.warning('You should use a unique identifier for cmd_id')
             if cmd is not None:
@@ -1021,7 +1052,7 @@ class OhmPi(object):
 
     def measure(self, *args, **kwargs):
         warnings.warn('This function is deprecated. Use load_sequence instead.', DeprecationWarning)
-        self.run_sequence(self, *args, **kwargs)
+        self.run_sequence(*args, **kwargs)
 
     def set_sequence(self, sequence=sequence):
         try:
@@ -1031,13 +1062,12 @@ class OhmPi(object):
             self.exec_logger.warning(f'Unable to set sequence: {e}')
             status = False
 
-    def run_sequence(self, **kwargs):
-        """Run sequence in sync mode
+    def run_sequence(self, cmd_id=None, **kwargs):
+        """Runs sequence in sync mode
         """
         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
@@ -1088,7 +1118,12 @@ class OhmPi(object):
         self.status = 'idle'
 
     def run_sequence_async(self, cmd_id=None, **kwargs):
-        """ Run the sequence in a separate thread. Can be stopped by 'OhmPi.interrupt()'.
+        """Runs the sequence in a separate thread. Can be stopped by 'OhmPi.interrupt()'.
+           
+            Parameters
+            ----------
+            cmd_id: 
+                
         """
         # self.run = True
         self.status = 'running'
@@ -1151,8 +1186,8 @@ class OhmPi(object):
         self.thread = threading.Thread(target=func)
         self.thread.start()
 
-    def run_multiple_sequences(self, cmd_id=None, **kwargs):
-        """ Run multiple sequences in a separate thread for monitoring mode.
+    def run_multiple_sequences(self, *args, **kwargs):
+        """Run multiple sequences in a separate thread for monitoring mode.
             Can be stopped by 'OhmPi.interrupt()'.
         """
         # self.run = True
@@ -1160,8 +1195,9 @@ class OhmPi(object):
         self.exec_logger.debug(f'Status: {self.status}')
         self.exec_logger.debug(f'Measuring sequence: {self.sequence}')
         cmd_id = kwargs.pop('cmd_id', None)
+        
         def func():
-            for g in range(0, self.settings["nb_meas"]): # for time-lapse monitoring
+            for g in range(0, self.settings["nb_meas"]):  # for time-lapse monitoring
                 if self.status != 'running':
                     self.exec_logger.warning('Data acquisition interrupted')
                     break
@@ -1236,10 +1272,12 @@ class OhmPi(object):
         self.interrupt()
 
     def interrupt(self):
-        """ Interrupt the acquisition. """
+        """Interrupts the acquisition. """
         self.status = 'stopping'
         if self.thread is not None:
             self.thread.join()
+        else:
+            self.exec_logger.debug('No sequence measurement thread to interrupt.')
         self.exec_logger.debug(f'Status: {self.status}')
 
     def quit(self):
@@ -1264,21 +1302,20 @@ print(colored(r' ________________________________' + '\n' +
               r'| | | |  _  || |\/| ||  __/  | |' + '\n' +
               r'\ \_/ / | | || |  | || |    _| |_' + '\n' +
               r' \___/\_| |_/\_|  |_/\_|    \___/ ', 'red'))
-print('OhmPi start')
 print('Version:', VERSION)
-platform, on_pi = OhmPi._get_platform()
+platform, on_pi = OhmPi._get_platform()  # noqa
 if on_pi:
-    print(colored(f'Running on {platform} platform', 'green'))
+    print(colored(f'\u2611 Running on {platform} platform', 'green'))
     # TODO: check model for compatible platforms (exclude Raspberry Pi versions that are not supported...)
     #       and emit a warning otherwise
     if not arm64_imports:
         print(colored(f'Warning: Required packages are missing.\n'
                       f'Please run ./env.sh at command prompt to update your virtual environment\n', 'yellow'))
 else:
-    print(colored(f'Not running on the Raspberry Pi platform.\nFor simulation purposes only...', 'yellow'))
+    print(colored(f'\u26A0 Not running on the Raspberry Pi platform.\nFor simulation purposes only...', 'yellow'))
 
 current_time = datetime.now()
-print(current_time.strftime("%Y-%m-%d %H:%M:%S"))
+print(f'local date and time : {current_time.strftime("%Y-%m-%d %H:%M:%S")}')
 
 # for testing
 if __name__ == "__main__":
diff --git a/requirements.txt b/requirements.txt
index 633c08af..941f7c69 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,12 +1,11 @@
 RPi.GPIO
 adafruit-blinka
-numpy
-paho-mqtt
 adafruit-circuitpython-adsgit 1x15
 adafruit-circuitpython-tca9548a
 adafruit-circuitpython-mcp230xx
+minimalmodbus
 gpiozero
+numpy
+paho-mqtt
 termcolor
-pyzmq
 pandas
-pyzmq
diff --git a/requirements_not_on_pi.txt b/requirements_not_on_pi.txt
index d26aec97..9e39a382 100644
--- a/requirements_not_on_pi.txt
+++ b/requirements_not_on_pi.txt
@@ -2,4 +2,3 @@ numpy
 paho-mqtt
 termcolor
 pandas
-pyzmq
-- 
GitLab