Example #1
0
class GroveMotor:

    def __init__(self, bus_n, bus_addr=0x0F, output='csv', name='Grove Motor Driver'):

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)

        # PWM clock frequency
        self.frequency = 31372

        # speed can be -255 to 255
        self.motor_1_speed = 0
        self.motor_2_speed = 0

        # direction of DC motor
        self.direction = ""

        # driver mode, 'dc' or 'step'
        self.mode = "dc"

        # phase of the motor being used, only applies in stepper mode
        self.phase = None

        # step code initialize
        self.step_codes_cw = None
        self.step_codes_ccw = None

        # motor phase steps, default to 2 phase
        self.set_phase(phase=2)

        # number of steps commanded
        self.steps = 0

        # running total steps, for positioning microsteps
        self.step_count = 0

        # information about this device
        self.metadata = Meta(name=name)
        self.metadata.description = ('I2C DC and stepper motor controller')
        self.metadata.urls = 'http://wiki.seeedstudio.com/Grove-I2C_Motor_Driver_V1.3/'
        self.metadata.manufacturer = 'Seeed Studio'
        
        self.metadata.header    = ['description', 'freq',
                                   'm1_speed', 'm2_speed', 'm_direction',
                                   'mode', 'phase', 'steps']
        self.metadata.dtype     = ['str', 'int', 'int', 'int', 'str', 'str', 'int', 'int']
        self.metadata.accuracy  = None
        self.metadata.precision = None
        
        self.metadata.bus_n = bus_n
        self.metadata.bus_addr = hex(bus_addr)
        
        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata, time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata, time_format='std_time_ms')
        
    def get_info(self):
        pid = self.bus.read_register_16bit(0x00)
        return pid

    def set_phase(self, phase=2):
        """Set the phase of the stepper motor being used.
        Defaults to a 2 phase.

        Parameters
        ----------
        phase : int, either 2 or 4 for the phase of the motor design
        """

        if phase == 4:
            # 4 phase motor
            self.step_codes_cw  = [0b0001, 0b0011, 0b0010, 0b0110,
                                   0b0100, 0b1100, 0b1000, 0b1001]
            self.step_codes_ccw = [0b1000, 0b1100, 0b0100, 0b0110,
                                   0b0010, 0b0011, 0b0001, 0b1001]
            self.phase = 4
        else:
            # default to 2 phase motor
            self.step_codes_cw  = [0b0001, 0b0101, 0b0100, 0b0110,
                                   0b0010, 0b1010, 0b1000, 0b1001]
            self.step_codes_ccw = [0b1000, 0b1010, 0b0010, 0b0110,
                                   0b0100, 0b0101, 0b0001, 0b1001]
            self.phase = 2

    def set_frequency(self, f_Hz=31372):
        """Set the frequency of the PWM cycle where
            cycle length = 510
            system_clock = 16MHz

        Available frequencies in Hz: 31372, 3921, 490, 122, 30

        Parameters
        ----------
        f_Hz : int, frequencey in Hertz (Hz).  Default is 31372
        """
        freq_mapper = {31372: 0x01, 3921: 0x02, 490: 0x03, 122: 0x04, 30: 0x05}
        self.frequency = freq_mapper[f_Hz]
        self.bus.write_n_bytes([0x84, self.frequency, 0x01])

    def set_speed(self, motor_id, motor_speed):
        """Set the speed (duty cycle) of motor

        Parameters
        ----------
        motor_id : int, either 1 or 2 where 1 = J1 and 2 = J5 on the board
        motor_speed : int, between -255 and 255 where > 0 is clockwise
        """
        assert (motor_speed > -256) & (motor_speed < 256)
        if motor_id == 1:
            self.motor_1_speed = motor_speed
        if motor_id == 2:
            self.motor_2_speed = motor_speed

        self.bus.write_n_bytes([0x82, self.motor_1_speed, self.motor_2_speed])

    def stop(self, motor_id=None):
        """Stop one or both motors.  Alias for set_speed(motor_id, motor_speed=0).

        Parameters
        ----------
        motor_id : int, (optional) 1 or 2 where 1 = J1 and 2 = J5 on the board.
            If not set, defaults to stopping both.
        """

        if (motor_id == 1) or (motor_id == 2):
            self.set_speed(motor_id=motor_id, motor_speed=0)
        else:
            self.set_speed(motor_id=1, motor_speed=0)
            self.set_speed(motor_id=2, motor_speed=0)

    def set_direction(self, code='cw'):
        """Set the direction of one or both motors in dc mode.

        Accepted string command codes are:
            'cw' : clockwise
            'ccw' : counter clockwise
            'm1m2cw' : motor 1 and motor 2 clockwise
            'm1m2cc' : motor 1 and motor 2 counter clockwise
            'm1cw_m2ccw' : motor 1 clockwise and motor 2 counter clockwise
            'm1ccw_m2cw' : motor 1 counter clockwise and motor 2 clockwise

        Parameters
        ----------
        code : str, command code, default is 'cw'
        """
        direction_mapper = {'cw': 0x0a, 'ccw': 0x05,
                            'm1m2cw': 0x0a, 'm1m2cc': 0x05,
                            'm1cw_m2ccw': 0x06, 'm1ccw_m2cw': 0x09}
        self.direction = direction_mapper[code]
        self.bus.write_n_bytes([0xaa, self.direction, 0x01])

    def set_mode(self, mode_type="dc"):
        if (mode_type == "full_step") or (mode_type == "micro_step"):
            self.set_speed(motor_id=1, motor_speed=255)
            self.set_speed(motor_id=2, motor_speed=255)
            self.mode = mode_type
        else:
            self.mode = "dc"

    def step_full(self, steps, delay=0.0001, verbose=False):
        """Stepper motor motion in full steps
        (four steps per step commanded)

        Parameters
        ----------
        steps : int, number of motor steps where
            positive numbers are clockwise
            negative numbers are counter clockwise
        delay : float, seconds to delay between step commands,
            default is 0.0001 seconds which smooths operation on the pi
        verbose : bool, print debug statements
        """

        self.steps = steps

        if steps > 0:
            step_codes = self.step_codes_cw
            self.direction = "step_cw"
        if steps < 0:
            step_codes = self.step_codes_ccw
            self.direction = "step_ccw"
        if steps == 0:
            self.stop()
            return

        self.set_mode(mode_type="full_step")  # set speed to 255, i.e. full current

        for _ in range(abs(steps)):
            for sc in step_codes:
                if verbose: print("{0:04b}".format(sc))
                self.bus.write_n_bytes([0xaa, sc, 0x01])
                time.sleep(delay)

        self.stop()  # set speed to 0, i.e. current off

    def step_micro(self, steps, delay=0.0001, verbose=False):
        """Stepper motor motion in micro steps
        (one step per step commanded)

        Parameters
        ----------
        steps : int, number of motor steps where
            positive numbers are clockwise
            negative numbers are counter clockwise
        delay : float, seconds to delay between step commands,
            default is 0.0001 seconds which smooths operation on the pi
        verbose : bool, print debug statements
        """

        self.steps = steps

        if steps > 0:
            step_codes = self.step_codes_cw
            self.direction = "step_cw"
        if steps < 0:
            step_codes = self.step_codes_ccw
            self.direction = "step_ccw"
        if steps == 0:
            self.stop()
            return

        self.set_mode(mode_type="micro_step")  # set speed to 255, i.e. full current

        for _ in range(abs(steps)):
            if verbose: print("n >>", _)
            ix = self.step_count * 2
            if verbose: print("ix >> ", ix)
            for sc in step_codes[ix:ix + 2]:
                if verbose: print("bin >> {0:04b}".format(sc))
                self.bus.write_n_bytes([0xaa, sc, 0x01])
                time.sleep(delay)
            self.step_count = (self.step_count + 1) % 4

        self.stop()  # set speed to 0, i.e. current off

    def get(self, description='no_description'):
        """Get formatted output.

        Parameters
        ----------
        description : char, description of data sample collected

        Returns
        -------
        data : list, data that will be saved to disk with self.write containing:
            description : str
        """
        return [description, self.frequency,
                self.motor_1_speed, self.motor_2_speed,
                self.direction, self.mode,
                self.phase, self.steps]

    def publish(self, description='no_description'):
        """Format output and save to file, formatted as either .csv or .json.

        Parameters
        ----------
        description : char, description of data sample collected

        Returns
        -------
        None, writes to disk the following data:
            description : str, description of sample
        """
        return self.json_writer.publish(self.get(description=description))

    def write(self, description='no_description'):
        """Format output and save to file, formatted as either .csv or .json.

        Parameters
        ----------
        description : char, description of data sample collected

        Returns
        -------
        None, writes to disk the following data:
            description : str, description of sample
        """
        wr = {"csv": self.csv_writer,
              "json": self.json_writer}[self.writer_output]
        wr.write(self.get(description=description))
Example #2
0
class MCP4728(object):
    def __init__(self, bus_n, bus_addr=0x60, output='csv', name='mcp4728'):
        """Initialize worker device on i2c bus.
        
        Note 1
        ------
        Default I2C bus address is 0x60 = 0b1100000
        
        From the dataset, section 5.3:
        The first part of the address byte consists of a 4-bit device code,
        which is set to 1100 for the MCP4728 device. The device code is 
        followed by three I2C address bits (A2, A1, A0) which are 
        programmable by the users.
        
        4-bit device code: 
            0b 1 1 0 0 

        Programmable user bits:
            0b A1 A2 A3 = 0b 0 0 0 

        (a) Read the address bits using “General Call Read
        Address” Command (This is the case when the
        address is unknown). See class self.read_address().
        (b) Write I2C address bits using “Write I2C Address
        Bits” Command. The Write Address command will replace 
        the current address with a new address in both input 
        registers and EEPROM. See class method self.XXXX().
        
        Note 2
        ------
        In most I2C cases, v_dd will be either 3.3V or 5.0V. The MCP4728 can
        handle as much as 24mA current at 5V (0.12W) in short circuit. 
        By comparison, the Raspberry Pi can source at most 16mA of current 
        at 3.3V (0.05W). Unless the application output will draw a very 
        small amount of current, an external (to the I2C bus) voltage source 
        should probably be used. 
        See the Adafruit ISO1540 Bidirectional I2C Isolator as a possible solution.
        
        Note 3
        ------
        The manufacturer uses the term VDD (Vdd) for external voltage, many other 
        sources use the term VCC (Vcc). VDD is used here to be consistent with the
        datasheet, but manufacturers like Adafruit use VCC on the pinouts labels.
        
        Parameters
        ----------
        bus_n : int, i2c bus number on Controller
        bus_addr : int, i2c bus number of this Worker device
        """

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)
        self.udac = 0
        self.state = {'a': None, 'b': None, 'c': None, 'd': None}
        self.v_dd = None

        # information about this device
        self.metadata = Meta(name=name)
        self.metadata.description = 'MCP4728 12-bit Digitial to Analog Converter'
        self.metadata.urls = 'http://ww1.microchip.com/downloads/en/DeviceDoc/22187E.pdf'
        self.metadata.manufacturer = 'Microchip'

        self.metadata.header = [
            'description', 'channel', 'v_ref_source', 'v_dd', 'power_down',
            'gain', 'input_code', 'output_voltage'
        ]
        self.metadata.dtype = [
            'str', 'str', 'str', 'float', 'str', 'int', 'int', 'float'
        ]
        self.metadata.units = [
            None, None, None, 'volts', None, None, None, 'volts'
        ]
        self.metadata.accuracy = None
        self.metadata.precision = 'vref: gain 1 = 0.5mV/LSB, gain 2 = 1mV/LSB; vdd: vdd/4096'

        self.metadata.bus_n = bus_n
        self.metadata.bus_addr = hex(bus_addr)

        # data recording method
        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata,
                                    time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata,
                                      time_format='std_time_ms')

    def general_call_reset(self):
        """Reset similar to power-on reset. See section 5.4.1."""
        self.bus.write_n_bytes(data=[0x00, 0x06])

    def general_call_wake_up(self):
        """Reset power-down bits. See section 5.4.2"""
        self.bus.write_n_bytes(data=[0x00, 0x09])

    def general_call_software_update(self):
        """Update all DAC analog outputs. See section 5.4.3"""
        self.bus.write_n_bytes(data=[0x00, 0x08])

    def general_call_read_address(self):
        """Return the I2C address. See section 5.4.4"""
        self.bus.write_n_bytes(data=[0x00, 0x0C])
        return self.bus.read_byte()

    def set_channel(self,
                    channel,
                    v_ref_source,
                    power_down,
                    gain,
                    input_code,
                    description='no description'):
        """Write single channel output
        
        Parameters
        ----------
        channel : str, either 'a', 'b', 'c' or 'd' corresponding 
            to the output channel
        v_ref_source : str, either 'internal' (2.048V/4.096V) or 'external' (VDD)
        power_down : str, either 'normal' or one of the power-down
            resistor to ground values of '1k' '100k' or '500k'
        gain : int, either 1 or 2 for multiplier relative to
            the internal reference voltage
        input_code : int, between 0 and 4095 to set the 
            output voltage
        """

        assert self.v_dd is not None, "External reference voltage, `v_dd` is not set."

        channel_map = {'a': 0b00, 'b': 0b01, 'c': 0b10, 'd': 0b11}
        v_ref_source_map = {'internal': 1, 'external': 0}
        power_down_map = {
            'normal': 0b00,
            '1k': 0b01,
            '100k': 0b10,
            '500k': 0b11
        }
        gain_map = {1: 0, 2: 1}

        # single write command
        # C2 = 0, C1 = 1, C0 = 0, W1 = 1, W0 = 1
        single_write_command = 0b01011000
        byte_2 = single_write_command + channel_map[channel] + self.udac

        byte_3 = ((v_ref_source_map[v_ref_source] << 7) +
                  (power_down_map[power_down] << 5) + (gain_map[gain] << 4) +
                  (input_code >> 8))

        byte_4 = input_code & 0b11111111

        self.bus.write_n_bytes(data=[byte_2, byte_3, byte_4])

        self.channel_state = self.channel_state[channel] = {
            'v_ref_source': v_ref_source,
            'power_down': power_down,
            'gain': gain,
            'input_code': input_code,
            'description': description
        }

        v_dd_to_v_ref_map = {'internal': 2.048 * gain, 'external': self.v_dd}
        v_ref_voltage = v_dd_to_v_ref_map[v_ref_source]
        output_voltage = self.output_voltage(input_code,
                                             v_ref_source=v_ref_source,
                                             gain=gain,
                                             v_dd=v_ref_voltage)

        # round to one more place than the precision of the chip
        # gain 1 = 0.5mV/LSB so 0.5mV = 0.0005V, therefore return 5 decimal places
        # gain 2 = 1mV/LSB so 1mV = 0.001V, therefore return 4 decimal places
        gain_rounder = {1: 5, 2: 4}
        output_voltage = round(output_voltage, gain_rounder[gain])

        self.state[channel] = [
            description, channel, v_ref_source, self.v_dd, power_down, gain,
            input_code, output_voltage
        ]

    @staticmethod
    def calculate_input_code(v_target, v_ref_source, gain, v_dd):
        """Calculate the required input register code
        required to produce a specific voltage

        Parameters
        ----------
        v_target : float, voltage target output on the DAC
        v_ref_source : str, either 'internal' (2.048V/4.096V) or 'external' (VDD)
        gain : int, gain of DAC, either 1 or 2
        v_dd : float, voltage source for the device. Could be I2C 3.3V or 5.0V, 
            or something else supplied on VDD

        Returns
        -------
        int, DAC inpute code required to achieve v_target
        """

        if v_ref_source == 'internal':
            if v_target > v_dd:
                return 'v_target must be <= v_dd'

            if (v_target > 2.048) & (gain == 1):
                return 'Gain must be 2 for v_target > v_ref internal'

            if (v_target > 4.096) & (gain == 2):
                return 'v_target must be <= 4.096V if using internal'

            return int((v_target * 4096) / (2.048 * gain))
        if v_ref_source == 'external':
            if v_target > v_dd:
                return 'v_target must be <= v_dd'

            return int((v_target * 4096) / v_dd)

    @staticmethod
    def output_voltage(input_code, v_ref_source, gain, v_dd=None):
        """Check output code voltage output

        Parameters
        ----------
        input_code : int, DAC inpute code required to achieve v_target
        v_ref_source : str, either 'internal' (2.048V/4.096V) or 'external' (VDD)
        gain : int, gain of DAC, either 1 or 2
        v_dd : float, voltage source for the device. Could be I2C 3.3V or 5.0V, 
            or something else supplied on VDD
        Returns
        -------
        v_target : float, DAC vol
        """

        if v_ref_source == 'internal':
            return (2.048 * input_code * gain) / 4096

        if v_ref_source == 'external':
            return (v_dd * input_code) / 4096

    @staticmethod
    def data_filler(data, cid):
        """Fill non-initialized channel data for publishing and writing

        Parameters
        ----------
        data : dict, self.state
        cid : str, channel id, one of 'a', 'b', 'c' or 'd'

        Returns
        -------
        list of lists, self.state data with None data filled 
            to match self.metadata.header
        """

        if data[cid] is None:
            return ['not_initialized', cid, None, None, None, None, None]
        else:
            return data[cid]

    def publish(self):
        """Output relay status data in JSON.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        str, formatted in JSON with keys:
            description: str, description of sample under test
            temperature : float, temperature in degrees Celcius
        """

        data_list = []
        for channel in ['a', 'b', 'c', 'd']:
            data = self.data_filler(self.state, channel)
            data_list.append(self.json_writer.publish(data))
        return data_list

    def write(self):
        """Format output and save to file, formatted as either
        .csv or .json.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1
        
        Returns
        -------
        None, writes to disk the following data:
            description : str, description of sample
            sample_n : int, sample number in this burst
            temperature : float, temperature in degrees Celcius
        """
        wr = {
            "csv": self.csv_writer,
            "json": self.json_writer
        }[self.writer_output]
        for channel in ['a', 'b', 'c', 'd']:
            data = self.data_filler(self.state, channel)
            wr.write(data)
Example #3
0
class TestDevice(Base):
    """Non-hardware test class"""
    def __init__(self, bus_n, bus_addr=0x00, output='json', name='software_test'):

        # data bus placeholder
        self.bus = 'fake I2C bus object on bus {} and address {}'.format(bus_n, bus_addr)

        # types of verbose printing
        self.verbose = False
        self.verbose_data = False

        # thread safe deque for sharing and plotting
        self.q_maxlen = 300
        self.q = deque(maxlen=self.q_maxlen)
        self.q_prefill_zeros = False

        # realtime/stream options
        self.go = False
        self.unlimited = False
        self.max_samples = 1000

        ## Metadata information about this device
        self.metadata = Meta(name=name)

        # device/source specific descriptions
        self.metadata.description  = 'dummy_data'
        
        # URL(s) for data source reference
        self.metadata.urls         = 'www.example.com'
        
        # manufacturer of device/source of data
        self.metadata.manufacturer = 'Nikola Tesla Company'
        
        ## data output descriptions
        # names of each kind of data value being recorded
        self.metadata.header       = ['description', 'sample_n', 'degree', 'amplitude']
        
        # data types (int, float, etc) for each data value
        self.metadata.dtype        = ['str', 'int', 'float', 'float']
        
        # measured units of data values
        self.metadata.units        = [None, 'count', 'degrees', 'real numbers']
        
        # accuracy in units of data values
        self.metadata.accuracy     = [None, 1, 0.2, 0.2] 
        
        # precision in units of data values
        self.metadata.precision    = [None, 1, 0.1, 0.1]
        
        # I2C bus the device is on
        self.metadata.bus_n = bus_n
        
        # I2C bus address the device is on
        self.metadata.bus_addr = bus_addr

        ## Data writers for this device
        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata, time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata, time_format='std_time_ms')
        
        # synthetic data
        self._deg = list(range(360))
        self._amp = [sin(d * (pi/180.0)) for d in self._deg]
        self._test_data = list(zip(self._deg, self._amp))
        
    @staticmethod
    def _cycle(iterable):
        """Copied from Python 3.7 itertools.cycle example"""
        saved = []
        for element in iterable:
            yield element
            saved.append(element)
        while saved:
            for element in saved:
                yield element

    def run(self, delay=0, index='count'):
        """Run data collection
        
        Parameters
        ----------
        delay : int, seconds to delay between returned data
        index : str, 'count' or 'timestamp'
        """

        if self.verbose:
            print('Test Started')
            print('='*40)
            
        # used in non-unlimited acquisition
        count = 0

        self.q.clear()

        if self.q_prefill_zeros:
            for _ in range(self.q_maxlen):
                self.q.append((0, 0))

        tp = TimePiece()
                
        if index == 'timestamp':
            def get_index():
                return tp.get_time()
        else:
            def get_index():
                return count
        
        if self.writer_output is not None:
            wr = {"csv": self.csv_writer,
                  "json": self.json_writer}[self.writer_output]
            
        for d, a in self._cycle(self._test_data):

            if not self.go:
                if self.verbose:
                    print("="*40)
                    print('Test Stopped')
                break

            self.metadata.state = 'Test run() method'

            i = get_index()

            data = [self.metadata.description, i, d, a]

            if self.writer_output is not None:
                wr.write(data)

            q_out = self.json_writer.publish(data)
            self.q.append(q_out)

            if self.verbose_data:
                print(q_out)

            if not self.unlimited:
                count += 1
                if count == self.max_samples:
                    self.go = False

            time.sleep(delay)
Example #4
0
class Single:

    def __init__(self, bus_n, bus_addr=0x18, output='csv', name='Qwiic_1xRelay'):

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)

        self.state_mapper = {0: "closed", 1: "open"}
        
        # information about this device
        self.metadata = Meta(name=name)
        self.metadata.description = 'Sparkfun Single Pole Double Throw Relay'
        self.metadata.urls = 'https://learn.sparkfun.com/tutorials/qwiic-single-relay-hookup-guide/all'
        self.metadata.manufacturer = 'Sparkfun'
        
        self.metadata.header    = ["description", "state"]
        self.metadata.dtype     = ['str', 'str']
        self.metadata.units     = None
        self.metadata.accuracy  = None 
        self.metadata.precision = None
        
        self.metadata.bus_n = bus_n
        self.metadata.bus_addr = hex(bus_addr)

        # data recording method
        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata, time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata, time_format='std_time_ms')
        
    def get_version(self):
        """Get the firmware version of the relay

        Returns
        -------
        int, version number of firmware
        """
        return self.bus.read_register_8bit(0x04)

    def get_status(self):
        """Get the status of the relay

        Returns
        -------
        int, where 0 == relay is open / not connected
                   1 == relay is closed / connected
        """

        return self.bus.read_register_8bit(0x05)

    def off(self):
        """Turn the relay off.  State will report 0."""
        self.bus.write_byte(0x00)

    def on(self):
        """Turn the relay on.  State will report 1."""
        self.bus.write_byte(0x01)

    def toggle(self, verbose=False):
        """Toggle state of relay
        if open, close
        if closed, open

        Parameters
        ----------
        verbose : bool, print debug statements
        """

        state = self.get_status()
        if verbose:
            print("Relay found {}.".format(self.state_mapper[state]))

        if state == 0:
            self.on()
        elif state == 1:
            self.off()

        if verbose:
            print("Relay toggled.")

    def change_address(self, new_address, verbose=False):
        """Change the I2C address of the relay.  This is a persistant change in
        EPROM memory.

        Parameters
        ----------
        new_address : int, new I2C address between 0x07 and 0x78
        verbose : bool, print debug statements
        """

        if ((new_address < 0x07) or (new_address > 0x78)):
            if verbose:
                print("Address outside allowed range")
            return False

        self.bus.write_register_8bit(0x03, new_address)
        if verbose:
            print("Relay I2C address changed to 0x{:02x}".format(new_address))

    def get(self, description='NA'):
        """Get output of relay state in list format.

        Parameters
        ----------
        description : char, description of data sample collected

        Returns
        -------
        data : list, data containing:
            description: str, description of sample under test
            state : int, where 0 == relay is open / not connected
                               1 == relay is closed / connected
        """
        return [description, self.get_status()]

    def publish(self, description='NA'):
        """Output relay status data in JSON.

        Parameters
        ----------
        description : char, description of data sample collected

        Returns
        -------
        str, formatted in JSON with keys:
            description : str, description of sample
            state : int, where 0 == relay is open / not connected
                               1 == relay is closed / connected
        """
        return self.json_writer.publish(self.get(description=description))

    def write(self, description='NA', delay=None):
        """Format output and save to file, formatted as either
        .csv or .json.

        Parameters
        ----------
        description : char, description of data sample collected

        Returns
        -------
        None, writes to disk the following data:
            description : str, description of sample
            state : int, where 0 == relay is open / not connected
                               1 == relay is closed / connected
        """
        wr = {"csv": self.csv_writer,
              "json": self.json_writer}[self.writer_output]
        wr.write(self.get(description=description))
Example #5
0
class SPS30():
    def __init__(self,
                 bus_n,
                 bus_addr=0x69,
                 output_format='float',
                 output='csv',
                 sensor_id='SPS30'):
        """Initialize worker device on i2c bus.

        Parameters
        ----------
        bus_n : int, i2c bus number on Controller
        bus_addr : int, i2c bus number of this Worker device
        output_format : str, what format measurements will be returned in
            either 'float' or 'int'
        output : str, writer output format, either 'csv' or 'json'
        sensor_id : str, sensor id, 'BME680' by default
        """

        self.output_format = None

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)

        # information about this device
        self.metadata = Meta(name='SPS30')
        self.metadata.description = 'SPS30 Particulate Matter Sensor'
        self.metadata.urls = 'https://www.sensirion.com/en/environmental-sensors/particulate-matter-sensors-pm25'
        self.metadata.manufacturer = 'Sensirion'
        self.metadata.bus_n = bus_n
        self.metadata.bus_addr = hex(bus_addr)
        self.metadata.speed_warning = 0
        self.metadata.laser_error = 0
        self.metadata.fan_error = 0

        self.set_format(output_format)

        # data recording information
        self.system_id = None
        self.sensor_id = sensor_id
        self.blocking_settle_dt = 15  # seconds
        self.blocking_timeout = 30  # seconds

        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata,
                                    time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata,
                                      time_format='std_time_ms')

    def set_format(self, output_format):
        """Set output format to either float or integer.

        Parameters
        ----------
        output_format : str, either 'int' or 'float' where
            'int' = Big-endian unsigned 16-bit integer values
            'float' = Big-endian IEEE754 float values
        """
        self.output_format = output_format

        # Note: precision reported is for the lower of the two concentration ranges listed in the datasheet
        self.metadata.header = [
            'system_id', 'sensor_id', 'description', 'sample_n', 'mc_pm_1_0',
            'mc_pm_2_5', 'mc_pm_4_0', 'mc_pm_10', 'nc_pm_0_5', 'nc_pm_1_0',
            'nc_pm_2_5', 'nc_pm_4_0', 'nc_pm_10', 'typical_partical_size'
        ]
        self.metadata.range = [
            'NA', 'NA', 'NA', 'NA', '0.3-1.0', '0.3-2.5', '0.3-4.0', '0.3-10',
            '0.3-0.5', '0.3-1.0', '0.3-2.5', '0.3-4.0', '0.3-10.0', '0-3000'
        ]
        self.metadata.precision = [
            'NA', 'NA', 'NA', '1', '+/-10', '+/-10', '+/-25', '+/-25',
            '+/-100', '+/-100', '+/-100', '+/-250', '+/-250', '1'
        ]

        self.metadata.mass_concentration_range = '0-1000µg/m3'
        self.metadata.number_concentration_range = '0-3000 #/cm3'

        # Note: accuracy is not specified from the manufacturer, calibration required to report accuracy
        self.metadata.accuracy = ['NA', 'NA', 'NA', '1'] + ['NA'] * 10

        _units = ['NA', 'NA', 'NA', 'count'] + ['µg/m3'] * 4 + ['#/cm3'] * 5
        if output_format == 'int':
            self.metadata.dtype = ['str', 'str', 'str', 'int'] + ['int'] * 10
            self.metadata.units = _units + ['nm']

        if output_format == 'float':
            self.metadata.dtype = ['str', 'str', 'str', 'int'] + ['float'] * 10
            self.metadata.units = _units + ['µm']

    def start_measurement(self):
        """Start measurement and set the output format. See ch 6.3.1

        Parameters
        ----------
        output_format : str, either 'int' or 'float' where
            'int' = Big-endian unsigned 16-bit integer values
            'float' = Big-endian IEEE754 float values
        """
        command = {
            'int': [0x05, 0x00, 0xF6],
            'float': [0x03, 0x00, 0xAC]
        }[self.output_format]
        self.bus.write_n_bytes([0x00, 0x10] + command)

    def stop_measurement(self):
        """Stop measurement. See ch 6.3.2"""
        self.bus.write_n_bytes([0x01, 0x04])

    def data_ready(self):
        """Read Data-Ready Flag. See ch 6.3.3

        Returns
        -------
        bool, True if data is ready, otherwise False
        """
        self.bus.write_n_bytes([0x02, 0x02])
        time.sleep(0.1)
        d = self.bus.read_n_bytes(3)
        d = CRC_check(d)
        if d[1] == 0x00:
            return False
        elif d[1] == 0x01:
            return True

    def measured_values_blocking(self, verbose=False):
        """Block and poll until new data is available

        Parameters
        ----------
        dt : int, seconds to pause between polling requests
        timeout : int, maximum seconds to poll
        # continuous : bool, if True do not stop measurement to read latest data
        verbose : bool, print debug statements
        """
        self.start_measurement()
        time.sleep(self.blocking_settle_dt)

        t0 = time.time()
        while (time.time() - t0 < self.blocking_timeout):
            if self.data_ready():
                self.stop_measurement()
                return self.measured_values()
            if verbose:
                print('waiting...')
        return False

    def measured_values(self, return_bytes=False):
        """Read measured values. See ch 6.3.4 for I2C method
        and ch 4.3 for format

        Parameters
        ----------
        bytes : bool, return bytes without CRC check or unpacking
        """
        byte_number = {'int': 30, 'float': 60}[self.output_format]
        self.bus.write_n_bytes([0x03, 0x00])

        # sleep long enough for the measurement and/or settling
        time.sleep(self.blocking_settle_dt + 0.1)

        d = self.bus.read_n_bytes(byte_number)
        if return_bytes:
            return d
        d = CRC_check(d)
        if self.output_format == 'float':
            d_f = []
            for n in range(0, len(d), 4):
                d_f.append(struct.unpack('>f', bytes(d[n:n + 4]))[0])
            d_f = [round(n, 2) for n in d_f]
            return d_f
        elif self.output_format == 'int':
            d_i = []
            for n in range(0, len(d), 2):
                d_i.append(struct.unpack('>h', bytes(d[n:n + 2]))[0])
            return d_i

    '''
    Features disabled due to I2C bus errors sending wake command

    def sleep(self):
        """Put sesnor to sleep. See ch 6.3.5
        Note: can only be executed while in Idle Mode"""
        self.bus.write_n_bytes([0x10, 0x01])

    def wake(self):
        """Power sensor up from sleep state. See ch 6.3.6"""
        self.bus.write_n_bytes([0x11, 0x03])
        self.bus.write_n_bytes([0x11, 0x03])
    '''

    def start_fan_cleaning(self):
        """Start fan cleaning cycle. See ch 6.3.7
        Can only be executed in Measurement Mode"""
        self.bus.write_n_bytes([0x56, 0x07])

    def read_cleaning_interval(self):
        """Read the fan cleaning cycle. See ch 6.3.8"""
        self.bus.write_n_bytes([0x80, 0x04])
        time.sleep(0.1)
        d = self.bus.read_n_bytes(6)
        d = CRC_check(d)
        return int((d[0] << 16) | d[1])

    def write_cleaning_interval(self, interval):
        """Write the cleaning interval. See ch 6.3.8 and 4.2"""
        i_msb = interval >> 16
        i_lsb = interval & (2**16 - 1)
        self.bus.write_n_bytes([0x80, 0x04, i_msb, i_lsb])

    def product_type(self):
        """Read product type. See ch 6.3.9"""
        self.bus.write_n_bytes([0XD0, 0X02])
        time.sleep(0.1)
        d = self.bus.read_n_bytes(12)
        d = CRC_check(d)
        d = [chr(x) for x in d if ascii_check(x)]
        return ''.join(d)

    def serial(self):
        """Read device serial number. See ch 6.3.9"""
        self.bus.write_n_bytes([0xD0, 0x33])
        time.sleep(0.1)
        d = self.bus.read_n_bytes(48)
        d = CRC_check(d)
        d = [chr(x) for x in d if ascii_check(x)]
        return ''.join(d)

    def version(self):
        """Get firmware version in major.minor notation. See ch 6.3.10"""
        self.bus.write_n_bytes([0xD1, 0x00])
        d = CRC_check(d)
        d = [chr(x) for x in d if ascii_check(x)]
        return '.'.join(d)

    def status(self):
        """Get status device status. See ch 4.4"""
        self.bus.write_n_bytes([0xD2, 0x06])
        time.sleep(0.1)
        d = self.bus.read_n_bytes(6)
        d = CRC_check(d)
        d = (d[0] << 8) + d[1]
        self.metadata.speed_warning = d >> 21 & 1
        self.metadata.laser_error = d >> 5 & 1
        self.metadata.fan_error = d >> 4 & 1
        return (self.metadata.speed_warning, self.metadata.laser_error,
                self.metadata.fan_error)

    def status_clear(self):
        """Clear device status. See ch 6.3.12"""
        self.bus.write_n_bytes([0xD2, 0x10])

    def reset(self):
        """Reset device. See ch 6.3.13"""
        self.bus.write_n_bytes([0xD3, 0x04])

    def publish(self, description='NA', n=1, delay=None, blocking=True):
        """Get measured air partical data and output in JSON,
        plus metadata at intervals set by self.metadata_interval

        Parameters
        ----------
        description : str, description of data collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1. This is
            in addition to any blocking settle time set.
        blocking : bool, if True wait until data is ready. If False,
            self.start_measurement and self.stop_measurement must be
            called externally to this method.

        Returns
        -------
        str, formatted in JSON with keys:
            description : str
            n : sample number in this burst
            and values as described in self.metadata.header
        """

        if blocking:
            get = self.measured_values_blocking
        else:
            get = self.measured_values

        data_list = []
        for m in range(n):
            _data = self.json_writer.publish(
                [self.system_id, self.sensor_id, description, m] + get())
            data_list.append(_data)
            if n == 1:
                return data_list[0]
            if delay is not None:
                time.sleep(delay)
        return data_list

    def write(self, description='NA', n=1, delay=None, blocking=True):
        """Get measured air partical data and save to file,
        formatted as either CSV with extension .csv or
        JSON and extension .jsontxt.

        Parameters
        ----------
        description : str, description of data collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1. This is
            in addition to any blocking settle time set.
        blocking : bool, if True wait until data is ready. If False,
            self.start_measurement and self.stop_measurement must be
            called externally to this method.

        Returns
        -------
        None, writes to disk the following data:
            description : str
            n : sample number in this burst
            and values as described in self.metadata.header
        """

        if blocking:
            get = self.measured_values_blocking
        else:
            get = self.measured_values

        wr = {
            "csv": self.csv_writer,
            "json": self.json_writer
        }[self.writer_output]
        for m in range(n):
            wr.write([self.system_id, self.sensor_id, description, m] + get())
            if delay is not None:
                time.sleep(delay)
Example #6
0
class SCD4x():
    def __init__(self, bus_n, bus_addr=0x62, output='csv', sensor_id='SCD4x'):
        """Initialize worker device on i2c bus.

        Parameters
        ----------
        bus_n : int, i2c bus number on Controller
        bus_addr : int, i2c bus number of this Worker device
        output : str, writer output format, either 'csv' or 'json'
        sensor_id : str, sensor id, 'BME680' by default
        """

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)

        # information about this device
        self.metadata = Meta(name='SCD4x')
        self.metadata.description = 'SCD4x CO2 gas Sensor'
        self.metadata.urls = 'https://www.sensirion.com/en/environmental-sensors/carbon-dioxide-sensors/carbon-dioxide-sensor-scd4x/'
        self.metadata.manufacturer = 'Sensirion'

        self.metadata.header = [
            'system_id', 'sensor_id', 'description', 'sample_n', 'co2', 'tC',
            'rh'
        ]
        self.metadata.dtype = [
            'str', 'str', 'str', 'int', 'int', 'float', 'int'
        ]
        self.metadata.units = [
            'NA', 'NA', 'NA', 'count', 'ppm', 'degrees Celsius', 'percent'
        ]
        self.metadata.accuracy = [
            'NA', 'NA', 'NA', '1', '+/- 40 + 5%', '+/- 1.5', '+/- 0.4'
        ]
        self.metadata.precision = [
            'NA', 'NA', 'NA', 'NA', '+/- 10', '+/- 0.1', '+/- 0.4'
        ]
        self.metadata.range = [
            'NA', 'NA', 'NA', 'NA', '0-40000', '-10-60', '0-100'
        ]

        self.metadata.bus_n = bus_n
        self.metadata.bus_addr = hex(bus_addr)

        # data recording information
        self.system_id = None
        self.sensor_id = sensor_id
        self.dt = None

        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata,
                                    time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata,
                                      time_format='std_time_ms')

    # Basic Commands, Ch 3.5
    def start_periodic_measurement(self):
        """Start periodic measurement, updating at a 5 second interval. See Ch 3.5.1"""
        self.bus.write_n_bytes([0x21, 0xB1])

    def read_measurement(self):
        """Read measurement from sensor. See Ch 3.5.2
        
        Returns
        -------
        co2 : int, CO2 concentration in ppm
        t : float, temperature in degrees Celsius
        rh : int, relative humidity in percent
        """
        self.bus.write_n_bytes([0xEC, 0x05])
        time.sleep(0.002)
        d = self.bus.read_n_bytes(9)
        d = CRC_check(d)

        co2 = d[0] << 8 | d[1]
        t = d[2] << 8 | d[3]
        rh = d[4] << 8 | d[5]

        t = -45 + 175 * t / 2**16
        rh = 100 * rh / 2**16

        d = [co2, t, rh]
        d = [round(n, 2) for n in d]
        return d

    def stop_periodic_measurement(self):
        """Stop periodic measurement to change configuration or 
        to save power. Note the sensor will only respond to other 
        commands after waiting 500ms after this command. See Ch 3.5.3"""
        self.bus.write_n_bytes([0x3F, 0x86])

    # On-chip output signal compensation, Ch 3.6
    def set_temperature_offset(self, tC):
        """Set the temperature offest, which only affects
        RH and T output. See Ch 3.6.1
        
        Parameters
        tC : float, temperature offset in degrees Celsius
        """
        tC = (tC * 2**16) / 175
        d0 = tC >> 8
        d1 = tC & 0xff
        d2 = CRC_check([d0, d1])
        self.bus.write_n_bytes([0x24, 0x1D, d0, d1, d2])
        time.sleep(0.01)

    def get_temperature_offset(self):
        """Get the temperature offset, which only affects 
        RH and T output. See Ch 3.6.2
        
        Returns
        -------
        float, temperature offset in degrees Celsius
        """
        self.bus.write_n_bytes([0x23, 0x18])
        time.sleep(0.01)
        d = self.bus.read_n_bytes(3)
        d = CRC_check(d)
        d = d[0] << 8 | d[1]
        return (175 * d) / 2**16

    def set_sensor_altitude(self, meters):
        """Set the altitude into memory. See Ch 3.6.3"""
        d0 = meters >> 8
        d1 = meters & 0xff
        d2 = CRC_check([d0, d1])
        self.bus.write_n_bytes([0x24, 0x27, d0, d1, d2])
        time.sleep(0.01)

    def get_sensor_altitude(self):
        """Get the altitude set in memory, this is not an 
        active measurement. See Ch 3.6.4
        
        Returns
        -------
        int, altitude in meters above sea level
        """
        self.bus.write_n_bytes([0x23, 0x22])
        time.sleep(0.002)
        d = self.bus.read_n_bytes(3)
        d = CRC_check(d)
        return d[0] << 8 | d[1]

    def set_ambient_pressure(self, pressure):
        """Set ambient pressure to enable continuous pressure
        compensation. See Ch 3.6.5
        
        Parameters
        ----------
        pressure : int, pressure in Pascals (Pa)
        """
        pressure = pressure / 100
        crc = CRC_calc([0x00, pressure])
        d = [0x00, pressure, crc]
        self.bus.write_n_bytes([0xE0, 0x00] + d)
        time.sleep(0.002)

    # Field Calibration, Ch 3.7
    def perform_forced_recalibration(self, target_ppm):
        """Perform forced recalibration (FRC). See Ch 3.7.1
        
        Parameters
        ----------
        target_ppm : int, target CO2 ppm
        
        Returns
        -------
        int, FRC correction in CO2 ppm
            or
        None if FRC failed 
        """
        self.bus.write_n_bytes([0x36, 0x2F])
        time.sleep(0.41)
        d = self.bus.read_n_bytes(3)
        d = CRC_check(d)
        d = d[0] << 8 | d[1]
        if d == 0xFFF:
            return None
        else:
            return d - 0x8000

    def set_automatic_self_calibration(self, asc):
        """Set Automatic Self Calibration (ASC) state. See Ch 3.7.2
        
        Parameters
        ----------
        asc : bool, True for ASC enabled, False for disabled
        """

        if asc == True:
            w0 = 0x01
        else:
            w0 = 0x00
        crc = CRC_calc([0x00, w0])
        d = [0x00, w0, crc]
        self.bus.write_n_bytes([0x24, 0x16] + d)
        time.sleep(0.002)

    def get_automatic_self_calibration(self):
        """Get Automatic Self Calibration (ASC) state. See Ch 3.7.3
        
        Returns
        -------
        bool, True if ASC is enabled, False for disabled
        """
        self.bus.write_n_bytes([0x23, 0x13])
        time.sleep(0.002)
        d = self.bus.read_n_bytes(3)
        d = CRC_check(d)
        d = d[0] << 8 | d[1]
        if d == 0:
            return False
        else:
            return True

    # Low Power, Ch 3.8
    def start_low_power_periodic_measuremet(self):
        """Start low power periodic measurement, updates in
        approximately 30 seconds. See Ch 3.8.1"""
        self.bus.write_n_bytes([0x21, 0xAC])

    def data_ready(self):
        """Check if data is ready. See Ch 3.8.2
        
        Returns
        -------
        bool, True if data is ready, otherwise False
        """
        self.bus.write_n_bytes([0xE4, 0xB8])
        d = self.bus.read_n_bytes(3)
        d = CRC_check(d)
        d = d[0] << 8 | d[1]
        d = d & 0b11111111111
        if d == 0:
            return False
        else:
            return True

    # Advanced Features, Ch 3.9
    def presist_settings(self):
        """Stores current configuration in the EEPROM making them 
        persist across power-cycling. To avoid failure of the EEPROM, 
        this feature should only be used as required and if actual 
        changes to the configuration have been made. See Ch 3.9.1"""
        self.bus.write_n_bytes([0x36, 0x15])
        time.sleep(0.8)

    def get_serial_number(self):
        """Read the serial number to identify the chip and 
        verify presense of the sensor. See Ch 3.9.2"""
        self.bus.write_n_bytes([0x36, 0x82])
        time.sleep(0.01)
        d = self.bus.read_n_bytes(9)
        d = CRC_check(d)
        da = []
        for n in range(0, len(d), 2):
            da.append(d[n] << 8 | d[n + 1])
        return da[0] << 32 | da[1] << 16 | da[2]

    def perform_self_test(self):
        """Perform a self test as an end-of-line test to check sensor
        functionality and the power supply to the sensor. See Ch 3.9.3"""
        self.bus.write_n_bytes([0x36, 0x39])
        time.sleep(11)
        d = self.bus.read_n_bytes(3)
        d = CRC_check(d)
        return d[0] << 8 | d[1]

    def perform_factory_reset(self):
        """Resets all configuration settings stored in EEPROM and 
        erases the FRC and ASC algorithm history. See Ch 3.9.4"""
        self.bus.write_n_bytes([0x36, 0x32])

    def reinit(self):
        """Reinitializes the sensor by reloading user settings from 
        EEPROM. See Ch 3.9.5"""
        self.bus.write_n_bytes([0x36, 0x46])

    # Low Power Single Shot, (SCD41 only), Ch 3.10
    def measure_single_shot(self):
        """On-demand measurement of CO2 concentration, relative humidity 
        and temperature. See Ch 3.10.1"""
        self.bus.write_n_bytes([0x21, 0x9D])
        time.sleep(5)

    def measure_single_shot_blocking(self):
        """On-demand measurement of CO2 concentration, relative humidity 
        and temperature. See Ch 3.10.1
        
        Returns
        -------
        co2 : int, CO2 concentration in ppm
        t : float, temperature in degrees Celsius
        rh : int, relative humidity in percent
        """

        t0 = time.time()
        self.measure_single_shot()
        time.sleep(5)  # 5000 ms
        self.dt = time.time() - t0
        return self.read_measurement()

    def read_measurement_blocking(self):
        """Read measurement from sensor. See Ch 3.5.2
        
        Returns
        -------
        co2 : int, CO2 concentration in ppm
        t : float, temperature in degrees Celsius
        rh : int, relative humidity in percent
        """
        pass

    def measure_single_shot_rht_only(self):
        """On-demand measurement of relative humidity and temperature only. 
        See Ch 3.10.2"""
        self.bus.write_n_bytes([0x21, 0x96])
        time.sleep(0.05)

    def publish(self, description='NA', n=1, delay=None, blocking=True):
        """Get measured air partical data and output in JSON, 
        plus metadata at intervals set by self.metadata_interval
        
        Parameters
        ----------
        description : str, description of data collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1
        blocking : bool, if True wait until data is ready. If False, 
            self.start_measurement and self.stop_measurement must be 
            called externally to this method.
        
        Returns
        -------
        str, formatted in JSON with keys:
            description : str
            n : sample number in this burst
            and values as described in self.metadata.header
        """

        if blocking:
            get = self.measure_single_shot_blocking
        else:
            get = self.read_measurement

        data_list = []
        for m in range(n):
            d = list(get())
            data_list.append(
                self.json_writer.publish(
                    [self.system_id, self.sensor_id, description, m] + d))
            if n == 1:
                return data_list[0]
            if delay is not None:
                time.sleep(delay)
        return data_list

    def write(self, description='NA', n=1, delay=None, blocking=True):
        """Get measured air partical data and save to file, 
        formatted as either CSV with extension .csv or 
        JSON and extension .jsontxt.

        Parameters
        ----------
        description : str, description of data collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1
        blocking : bool, if True wait until data is ready. If False, 
            self.start_measurement and self.stop_measurement must be 
            called externally to this method.

        Returns
        -------
        None, writes to disk the following data:
            description : str
            n : sample number in this burst
            and values as described in self.metadata.header
        """

        if blocking:
            get = self.measure_single_shot_blocking
        else:
            get = self.read_measurement

        wr = {
            "csv": self.csv_writer,
            "json": self.json_writer
        }[self.writer_output]
        for m in range(n):
            d = list(get())
            wr.write([self.sytem_id, self.sensor_id, self.description, m] + d)
            if delay is not None:
                time.sleep(delay)
Example #7
0
class MCP23008:
    def __init__(self, bus_n, bus_addr=0x20, output='csv'):
        """Initialize worker device on i2c bus.

        Parameters
        ----------
        bus_n : int, i2c bus number on Controller
        bus_addr : int, i2c bus number of this Worker device
        output : str, output data format, either 'csv' (default) or 'json'
        """

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)

        # set direction of I/O to output for all pins
        self.bus.write_register_8bit(reg_addr=0x00, data=0)

        self.reg_olat = None

        # information about this device
        self.metadata = Meta('MCP23008')
        self.metadata.description = '8 channel I2C relay board by Peter Jakab'
        self.metadata.urls = 'https://jap.hu/electronic/relay_module_i2c.html'
        self.metadata.manufacturer = 'Peter Jakab'

        self.metadata.header = ['description', 'sample_n', 'relay_state']
        self.metadata.dtype = ['str', 'int', 'str']
        self.metadata.accuracy = None
        self.metadata.precision = None

        self.metadata.bus_n = bus_n
        self.metadata.bus_addr = hex(bus_addr)

        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata,
                                    time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata,
                                      time_format='std_time_ms')

    def get_all_channels(self):
        """Get all channel states, as a single 8 bit value. Each bit 
        represents one channel where 0 = On, 1 = Off.
        
        Returns
        -------
        state : int, 8 bits
        """
        self.reg_olat = self.bus.read_register_8bit(reg_addr=0x0A)
        time.sleep(0.01)
        return self.reg_olat

    def set_all_channels(self, state):
        """Set all channel state, as a single 8 bit value. Each bit 
        represents one channel where 0 = On, 1 = Off.
        
        Parameters
        ----------
        state : int, 8 bits
        """
        self.bus.write_register_8bit(reg_addr=0x0A, data=state)

    def set_channel(self, channel, state):
        """Set a single channel On, Off or Toggle it
        
        Parameters
        ----------
        channel : int, 1-8 for the channel to change
        state : int, where
            0 = On
            1 = Off
            2 = Toggle from last state
        """

        assert (channel > 0) & (channel < 9)

        bitvalue = (1 << (channel - 1))

        self.get_all_channels()

        if state == 0:
            self.reg_olat &= (~bitvalue)
        elif state == 1:
            self.reg_olat |= bitvalue
        elif state == 2:
            self.reg_olat ^= bitvalue

        self.set_all_channels(state=self.reg_olat)

    def publish(self, description='NA'):
        """Get relay state and output in JSON, plus metadata at intervals 
        set by self.metadata_interval

        Parameters
        ----------
        description : char, description of data sample collected, default='NA'

        Returns
        -------
        description : str
        n : sample number in this burst
        state : str, binary boolean state for each channel where
            0 = On
            1 = Off
        """
        m = 0
        state = self.get_all_channels()
        state = tools.left_fill(s=bin(state)[2:], n=8, x="0")
        return self.json_writer.publish([description, m, state])

    def write(self, description='NA'):
        """Get ADC output and save to file, formatted as either .csv or .json.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        None, writes to disk the following data:
            description : str
            n : sample number in this burst
            state : str, binary boolean state for each channel where
                0 = On
                1 = Off
        """
        wr = {
            "csv": self.csv_writer,
            "json": self.json_writer
        }[self.writer_output]
        m = 1
        state = self.get_all_channels()
        state = "0b" + tools.left_fill(s=bin(state)[2:], n=8, x="0")
        wr.write([description, m, state])
Example #8
0
class BME680:
    def __init__(self, bus_n, bus_addr=0x77, output='csv', sensor_id='BME680'):
        """Initialize worker device on i2c bus.

        For register memory map, see datasheet pg 28, section 5.2

        Parameters
        ----------
        bus_n : int, i2c bus number on Controller
        bus_addr : int, i2c bus number of this Worker device
        output : str, writer output format, either 'csv' or 'json'
        sensor_id : str, sensor id, 'BME680' by default
        """

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)

        # Default oversampling and filter register values.
        self.refresh_rate = 1
        self.filter = 1

        # memory mapped from 8 bit register locations
        self.mode = 0b00  # 2 bits, ctrl_meas <1:0>  operation mode
        self.osrs_p = 0b001  # 3 bits, ctrl_meas <4:2>, oversample pressure
        self.osrs_t = 0b001  # 3 bits, ctrl_meas <2:0>, oversample temperature
        self.osrs_h = 0b001  # 3 bits, ctrl_hum <2:0>,  oversample humidity
        self.run_gas = 0b0  # 1 bit,  ctrl_gas_1 <4>,  run gas measurement
        self.nb_conv = 0b000  # 4 bits, ctrl_gas_1 <3:0> conversion profile number
        self.heat_off = 0b0  # 1 bit,  ctrl_gas_0 <3>,  gas heater on/off

        # profile registers
        self.gas_wait_x = None  # gas_wait registers 9-0
        self.res_heat_x = None  # res_heat registers 9-0
        self.idac_heat_x = None  # idac_heat registers 9-0

        # calibration and state
        self._temp_calibration = None
        self._pressure_calibration = None
        self._humidity_calibration = None
        self._gas_calibration = None
        self.gas_meas_index = None
        self._new_data = None
        self._gas_measuring = None
        self._measuring = None
        self._heat_range = None
        self._heat_val = None
        self._heat_stab = None
        self._range_switch_error = None

        # raw ADC values
        self._adc_pres = None
        self._adc_temp = None
        self._adc_hum = None
        self._adc_gas = None
        self._gas_range = None
        self._t_fine = None

        # valid data flags
        self._gas_valid = None
        self._head_stab = None

        # control registers
        self._r_ctrl_meas = None
        self._r_ctrl_gas_1 = None

        # the datasheet gives two options: float or int values and equations
        # this code uses integer calculations, see table 16
        self._const_array1_int = (2147483647, 2147483647, 2147483647,
                                  2147483647, 2147483647, 2126008810,
                                  2147483647, 2130303777, 2147483647,
                                  2147483647, 2143188679, 2136746228,
                                  2147483647, 2126008810, 2147483647,
                                  2147483647)
        self._const_array2_int = (4096000000, 2048000000, 1024000000,
                                  512000000, 255744255, 127110228, 64000000,
                                  32258064, 16016016, 8000000, 4000000,
                                  2000000, 1000000, 500000, 250000, 125000)

        # Pressure in hectoPascals at sea level, used to calibrate altitude
        self.sea_level_pressure = 1013.25

        # calculated ambient temperature for res_heat target calculation
        self.amb_temp = None

        # sample collection metadata
        self._last_reading = 0
        self._min_refresh_time = 1 / self.refresh_rate

        # information about this device
        self.metadata = Meta(name='BME680')
        self.metadata.description = 'Bosch Humidity, Pressure, Temperature, VOC Sensor'
        self.metadata.urls = 'https://www.bosch-sensortec.com/products/environmental-sensors/gas-sensors-bme680/'
        self.metadata.manufacturer = 'Bosch Sensortec'

        self.metadata.header = [
            'system_id', 'sensor_id', 'description', 'sample_n', 'T', 'P',
            'RH', 'g_res', 'g_val', 'heat_stab'
        ]
        self.metadata.dtype = [
            'str', 'str', 'str', 'int', 'float', 'float', 'float', 'float',
            'bool', 'bool'
        ]
        self.metadata.units = [
            'NA',
            'NA',
            'NA',
            'count',
            'Celcius',
            'hectopascals',
            'percent',
            'ohms',
            'NA',
            'NA',
        ]
        self.metadata.accuracy = [
            'NA', 'NA', 'NA', '1', '+/-1.0', '+/-0.12', '+/-3', '+/-15%', 'NA',
            'NA'
        ]
        self.metadata.precision = [
            'NA', 'NA', 'NA', '1', '0.1', '0.18', '0.008', '0.08', 'NA', 'NA'
        ]
        self.metadata.bus_n = bus_n
        self.metadata.bus_addr = hex(bus_addr)

        # data recording information
        self.system_id = None
        self.sensor_id = sensor_id

        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata,
                                    time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata,
                                      time_format='std_time_ms')

    def read_calibration(self):
        """Read chip calibration coefficients

        Coefficients are not listed in Table 20: Memory Map.  Instead they are
        referenced in Tables 11, 12, 13 and 14.
        """
        coeff = self.bus.read_register_nbit(0x89, 25)
        coeff += self.bus.read_register_nbit(0xE1, 16)

        coeff = list(
            struct.unpack('<hbBHhbBhhbbHhhBBBHbbbBbHhbb', bytes(coeff[1:39])))
        coeff = [float(i) for i in coeff]

        # 3 bytes
        self._temp_calibration = [coeff[x] for x in [23, 0, 1]]
        # 10 bytes
        self._pressure_calibration = [
            coeff[x] for x in [3, 4, 5, 7, 8, 10, 9, 12, 13, 14]
        ]
        # 7 bytes
        self._humidity_calibration = [
            coeff[x] for x in [17, 16, 18, 19, 20, 21, 22]
        ]
        # 3 bytes
        self._gas_calibration = [coeff[x] for x in [25, 24, 26]]

        # current method = 39 byte read + 3 byte setup

        # flip around H1 & H2
        self._humidity_calibration[1] *= 16
        self._humidity_calibration[1] += self._humidity_calibration[0] % 16
        self._humidity_calibration[0] /= 16

        self._range_switch_error = self.bus.read_register_8bit(0x04)
        self._heat_range = (self.bus.read_register_8bit(0x02) & 0x30) / 16
        self._heat_val = self.bus.read_register_8bit(0x00)

    def set_oversampling(self, h, t, p):
        """Set oversample rate for temperature, pressures and
        humidity.  Valid values for all three are:
            0, 1, 2, 4, 8, 16
            Note: 0 will skip measurement

        Parameters
        ----------
        h : int, oversampling rate of humidity
        t : int, oversampling rate of temperature
        p : int, oversampling rate of pressure
        """

        htp_mapper = {
            0: 0b000,
            1: 0b001,
            2: 0b010,
            4: 0b011,
            8: 0b100,
            16: 0b101
        }

        # ctrl_hum register, 0x72
        self.osrs_h = htp_mapper[h]
        self.write_r_ctrl_hum()

        # ctrl_meas register, 0x74
        self.osrs_t = htp_mapper[t]
        self.osrs_p = htp_mapper[p]
        self.write_r_ctrl_meas()

    # Register Methods
    # In the order listed in Table 20: Memory Map, pg28

    def reset(self):
        """Reset the device using a soft-reset procedure, which has the
        same effect as a power-on reset, by writing register
        'reset' at 0xE0 with value 0xB6.  Default value is 0x00."""
        self.bus.write_n_bytes([0xE0, 0xB6])
        time.sleep(0.005)

    def read_r_chip_id(self):
        """Check that the chip ID is correct.  Should return 0x61"""
        _chip_id = self.bus.read_register_8bit(0xD0)
        if _chip_id != 0x61:
            raise OSError('Expected BME680 ID 0x%x, got ID: 0x%x' %
                          (0x61, _chip_id))

    def read_r_config(self):
        """Read register 'config' at 0x75 which contains the
        'filter' contorl register.  'spi_3w_en' is always
        0b0 in I2C mode"""
        _config = self.bus.read_register_8bit(0x75)
        self.filter = (_config >> 2) & 0b111  # mask to be sure

    def read_r_ctrl_meas(self):
        """Read register 'ctrl_meas' at 0x74 which contains the
        'mode', 'osrs_p' and 'osrs_t' control registers"""
        _ctrl_meas = self.bus.read_register_8bit(0x74)
        self.mode = _ctrl_meas & 0b11
        self.osrs_p = (_ctrl_meas >> 2) & 0b111
        self.osrs_t = _ctrl_meas >> 5

    def write_r_ctrl_meas(self):
        """Write register 'ctrl_meas' at 0x74 which contains the
        'mode', 'osrs_p' and 'osrs_t' control registers"""
        _ctrl_meas = (((self.osrs_t << 5) | (self.osrs_p << 2)) | self.mode)
        self.bus.write_n_bytes([0x74, _ctrl_meas])

    def read_r_ctrl_hum(self):
        """Read register 'ctrl_hum' at 0x72 which contains the
        'osrs_h' control register.  'spi_3w_int_en' is always
        0b0 in I2C mode"""
        _ctrl_hum = self.bus.read_register_8bit(0x72)
        self.osrs_h = _ctrl_hum & 0b111  # mask just to be sure

    def write_r_ctrl_hum(self):
        """Write register 'ctrl_hum' at 0x72 which contains the
        'osrs_h' control register.  'spi_3w_int_en' is always
        0b0 in I2C mode"""
        self.bus.write_n_bytes([0x72, self.osrs_h])

    def read_r_ctrl_gas_1(self):
        """Contains 'run_gas' and 'nb_conv' control registers"""
        _ctrl_gas_1 = self.bus.read_register_8bit(0x71)
        self.run_gas = (_ctrl_gas_1 >> 4) & 0b1
        self.nb_conv = _ctrl_gas_1 & 0b1111

    def write_r_ctrl_gas_1(self):
        """Contains 'run_gas' and 'nb_conv' control registers"""
        _ctrl_gas_1 = self.nb_conv | (self.run_gas << 4)
        self.bus.write_n_bytes([0x71, _ctrl_gas_1])

    def read_r_ctrl_gas_0(self):
        """Contains the 'heat_off' control register"""
        _ctrl_gas_0 = self.bus.read_register_8bit(0x70)
        self.heat_off = (_ctrl_gas_0 >> 3) & 0b1

    def write_r_ctrl_gas_0(self):
        _ctrl_gas_0 = self.heat_off << 4
        self.bus.write_register_8bit(0x70, _ctrl_gas_0)

    def reg_gas_wait_x(self):
        """Control register for gas wait profiles"""
        self.gas_wait_x = self.bus.read_n_bytes(0x64, 10)

    def reg_res_heat_x(self):
        """Control register for heater resistance profiles"""
        self.res_heat_x = self.bus.read_n_bytes(0x5A, 10)

    def reg_idac_heat_x(self):
        """Control register for heater current profiles"""
        self.idac_heat_x = self.bus.read_n_bytes(0x50, 10)

    def mode_sleep(self):
        """Set chip mode to Sleep Mode"""
        self.bus.write_n_bytes([])

    def mode_forced(self):
        """Set chip mode to Forced Mode (active for measurement)"""
        self.bus.write_n_bytes([])

    # Operation Methods
    def gas_on(self):
        self.run_gas = 0b1
        self.write_r_ctrl_gas_1()

    def gas_off(self):
        self.run_gas = 0b0
        self.write_r_ctrl_gas_1()

    def forced_mode(self):
        self.mode = 0b01
        self.write_r_ctrl_meas()

    def sleep_mode(self):
        self.mode = 0b00
        self.write_r_ctrl_meas()

    def set_filter(self, coeff):
        """Set the temperature and pressure IIR filter

        Parameters
        ----------
        coeff : int, filter coefficient.  Valid values are:
            0, 1, 3, 7, 15, 31, 63, 127
        """
        mapper = {
            0: 0b000,
            1: 0b001,
            3: 0b010,
            7: 0b011,
            15: 0b100,
            31: 0b101,
            63: 0b110,
            127: 0b111
        }
        self.filter = coeff
        _filter = mapper[self.filter]
        self.bus.write_n_bytes([0x75, _filter << 2])  # config register

    def set_x_register(self, reg_0, n, value):
        """Set register within one of the three 10 byte registers:
            Gas_wait_x  : gas_wait_9  @ 0x6D downto  gas_wait_0 @ 0x64
            Res_heat_x  : res_heat_0  @ 0x63 downto  res_heat_0 @ 0x5A
            Idac_heat_x : idac_heat_9 @ 0x59 downto idac_heat_0 @ 0x50
        See Table 20 Memory Map for details.

        Parameters
        ----------
        n : int, set the nth register within the register block
        reg_0 : int, 0th register of the register block
        value : int, value for register
        """
        self.bus.write_register_8bit(n + reg_0, value)

    def set_gas_wait(self, n, value):
        """Set gas wait time for Gas_wait_x registers.
        Registers range from 0 = 0x64 up to 9 = 0x6D

        Parameters
        ----------
        n : int, set the nth register within the register block
        value : int, value for register
        """
        self.set_x_register(reg_0=0x64, n=n, value=value)

    def set_res_heat(self, n, value):
        """Set resistance for the heater registers.
        Registers range from 0 = 0x64 up to 9 = 0x6D

        Parameters
        ----------
        n : int, set the nth register within the register block
        value : int, value for register
        """
        self.set_x_register(reg_0=0x5A, n=n, value=value)

    def get_gas_wait(self):
        """Gas_wait_x  : gas_wait_9  @ 0x6D downto  gas_wait_0 @ 0x64

        Returns
        -------
        bytes : 10 bytes from register range reg_0 to reg_0 + 10"""
        return self.bus.read_register_nbit(0x64, 10)

    def get_res_heat(self):
        """Res_heat_x  : res_heat_0  @ 0x63 downto  res_heat_0 @ 0x5A

        Returns
        -------
        bytes : 10 bytes from register range reg_0 to reg_0 + 10"""
        return self.bus.read_register_nbit(0x5A, 10)

    def get_idac_heat(self):
        """Idac_heat_x : idac_heat_9 @ 0x59 downto idac_heat_0 @ 0x50

        Returns
        -------
        bytes : 10 bytes from register range reg_0 to reg_0 + 10"""
        return self.bus.read_register_nbit(0x50, 10)

    def calc_res_heat(self, target_temp):
        """Convert a target temperature for the heater to a resistance
        target for the chip

        Parameters
        ----------
        target_temp : int, target temperature in degrees Celcius

        Returns
        -------
        res_heat : int, register code for target temperature
        """

        if self.amb_temp is None:
            data = self.bus.read_register_nbit(0x22, 3)  # 0x22
            self._adc_temp = ((data[0] << 16) + (data[1] << 8) + data[2]) >> 4
            self.amb_temp = self.temperature()

        amb_temp = int(self.amb_temp)

        par_g1 = self.bus.read_register_8bit(0xED)
        par_g2_lsb = self.bus.read_register_8bit(0xEB)
        par_g2_msb = self.bus.read_register_8bit(0xEC)
        par_g2 = (par_g2_msb << 8) + par_g2_lsb
        par_g3 = self.bus.read_register_8bit(0xEE)

        res_heat_range = self.bus.read_register_8bit(0x02)
        res_heat_range = (res_heat_range >> 4) & 0b11
        res_heat_val = self.bus.read_register_8bit(0x00)

        var1 = ((amb_temp * par_g3) // 10) << 8
        var2 = ((par_g1 + 784) * ((((
            (par_g2 + 154009) * int(target_temp) * 5) // 100) + 3276800) // 10)
                )
        var3 = var1 + (var2 >> 1)
        var4 = var3 // (res_heat_range + 4)
        var5 = (131 * res_heat_val) + 65536
        res_heat_x100 = int(((var4 // var5) - 250) * 34)
        res_heat_x = int((res_heat_x100 + 50) // 100)

        return res_heat_x

    @staticmethod
    def calc_wait_time(t, x):
        """Calculate the wait time code for a heating profile

        Parameters
        ----------
        t : int, valued 0-63 with 1 ms step sizes, 0 = no wait
        x : int, multiplier for x, one of 1, 4, 16, 64

        Returns
        -------
        byte : register value
        """
        assert t < 63
        mapper = {1: 0b00, 4: 0b01, 16: 0b10, 64: 0b11}
        x = mapper[x]
        return (x << 6) | t

    def get_measurement_status(self):
        reg_meas_status = self.bus.read_register_8bit(0x1D)
        self.gas_meas_index = reg_meas_status & 0b1111
        self._new_data = reg_meas_status >> 7
        self._gas_measuring = (reg_meas_status >> 6) & 0b1
        self._measuring = (reg_meas_status >> 5) & 0b1

    def measure(self, verbose=False):
        """Get the temperature, pressure and humidity"""
        self._new_data = 0

        t0 = time.time()
        cx = 1
        while True:
            self.get_measurement_status()
            dt = (time.time() - t0)

            if self._measuring == 1:
                if verbose:
                    print('Still measuring, waiting 1 second...')
                time.sleep(1)

            elif self._new_data == 1:
                if verbose:
                    print('New data found!')
                break

            elif dt > 5:
                if verbose:
                    print('Timeout waiting for new data :(')
                return False
            if verbose:
                print('While loop %s' % cx)
            cx += 1

        data = self.bus.read_register_nbit(0x1F, 3)  # 0x1F
        self._adc_pres = ((data[0] << 16) + (data[1] << 8) + data[2]) >> 4

        data = self.bus.read_register_nbit(0x22, 3)  # 0x22
        self._adc_temp = ((data[0] << 16) + (data[1] << 8) + data[2]) >> 4

        data = self.bus.read_register_nbit(0x25, 3)  # 0x25
        self._adc_hum = (data[0] << 8) + data[1]

        _gas_r_msb = self.bus.read_register_8bit(0x2A)
        _gas_r_lsb = self.bus.read_register_8bit(0x2B)

        self._adc_gas = (_gas_r_msb << 2) + (_gas_r_lsb >> 6)
        self._gas_valid = (_gas_r_lsb >> 5) & 0b1
        self._heat_stab = (_gas_r_lsb >> 4) & 0b1
        self._gas_range = _gas_r_lsb & 0b1111  # 0x2B <4:0>
        return True

    def gas(self):
        """Calculate the gas resistance in ohms"""
        var1 = ((1340 + (5 * self._range_switch_error)) *
                (self._const_array1_int[self._gas_range])) >> 16

        var2 = (self._adc_gas << 15) - 16777216 + var1  # 1 << 24 = 16777216

        gas_res = (((self._const_array2_int[self._gas_range] * var1) >> 9) +
                   (var2 >> 1)) / var2
        return int(gas_res)  #, self._adc_gas, self._gas_range, var1, var2

    def temperature(self):
        """Calculate the compensated temperature in degrees celsius"""
        var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2)
        var2 = (var1 * self._temp_calibration[1]) / 2048
        var3 = ((var1 / 2) * (var1 / 2)) / 4096
        var3 = (var3 * self._temp_calibration[2] * 16) / 16384
        self._t_fine = int(var2 + var3)
        calc_temp = (((self._t_fine * 5) + 128) / 256) / 100
        return calc_temp

    def pressure(self):
        """Calculate the barometric pressure in hectoPascals"""
        var1 = (self._t_fine / 2) - 64000
        var2 = ((var1 / 4) * (var1 / 4)) / 2048
        var2 = (var2 * self._pressure_calibration[5]) / 4
        var2 = var2 + (var1 * self._pressure_calibration[4] * 2)
        var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536)
        var1 = (((((var1 / 4) * (var1 / 4)) / 8192) *
                 (self._pressure_calibration[2] * 32) / 8) +
                ((self._pressure_calibration[1] * var1) / 2))
        var1 = var1 / 262144
        var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768
        calc_pres = 1048576 - self._adc_pres
        calc_pres = (calc_pres - (var2 / 4096)) * 3125
        calc_pres = (calc_pres / var1) * 2
        var1 = (self._pressure_calibration[8] *
                (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096
        var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192
        var3 = ((
            (calc_pres / 256)**3) * self._pressure_calibration[9]) / 131072
        calc_pres += ((var1 + var2 + var3 +
                       (self._pressure_calibration[6] * 128)) / 16)
        calc_pres = calc_pres / 100
        return calc_pres

    def humidity(self):
        """The relative humidity in RH %"""
        temp_scaled = ((self._t_fine * 5) + 128) / 256
        var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) -
                ((temp_scaled * self._humidity_calibration[2]) / 200))
        var2 = (self._humidity_calibration[1] * (
            ((temp_scaled * self._humidity_calibration[3]) / 100) +
            (((temp_scaled *
               ((temp_scaled * self._humidity_calibration[4]) / 100)) / 64) /
             100) + 16384)) / 1024
        var3 = var1 * var2
        var4 = self._humidity_calibration[5] * 128
        var4 = (var4 +
                ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16
        var5 = ((var3 / 16384) * (var3 / 16384)) / 1024
        var6 = (var4 * var5) / 2
        calc_hum = (((var3 + var6) / 1024) * 1000) / 4096
        calc_hum /= 1000  # get back to RH

        if calc_hum > 100:
            calc_hum = 100
        if calc_hum < 0:
            calc_hum = 0
        return calc_hum

    def get(self, description='NA', n=1, verbose=False):
        """Get one sample of data

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        verbose : bool, print debug statements

        Returns
        -------
        data : list, data containing:
            description: str, description of sample under test
            sample_n : int, sample number in this burst
            t : float, temperature in degress C
            p : float, pressure in hectoPascals
            h : float, relative humidty in percent
            g : float, gas resistence
            g_valid : int, gas value is valid
            heat_stable : int, gas heater is stable
        """
        self.forced_mode()
        time.sleep(0.2)

        if not self.measure(verbose):
            return False
        t = self.temperature()
        p = self.pressure()
        h = self.humidity()
        g = self.gas()
        d = [t, p, h, g]
        d = [round(n, 2) for n in d]
        return [self.system_id, self.sensor_id, description, n
                ] + d + [self._gas_valid, self._heat_stab]

    def publish(self, description='NA', verbose=False):
        """Get one sample of data in JSON.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        verbose : bool, print debug statements

        Returns
        -------
        str, formatted in JSON with keys:
            description: str, description of sample under test
            sample_n : int, sample number in this burst
            t : float, temperature in degress C
            p : float, pressure in hectoPascals
            h : float, relative humidty in percent
            g : float, gas resistence
            g_valid : int, gas value is valid
            heat_stable : int, gas heater is stable
        """
        data = self.get(description=description, verbose=verbose)
        json_data = self.json_writer.publish(data)
        return json_data

    def write(self, description='NA', n=1, delay=None):
        """Format output and save to file, formatted as either .csv or .json.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        None, writes to disk the following data:
            description: str, description of sample under test
            sample_n : int, sample number in this burst
            t : float, temperature in degress C
            p : float, pressure in hectoPascals
            h : float, relative humidty in percent
            g : float, gas resistence
            g_valid : int, gas value is valid
            heat_stable : int, gas heater is stable
        """
        wr = {
            "csv": self.csv_writer,
            "json": self.json_writer
        }[self.writer_output]
        for m in range(n):
            data = self.get(description=description)
            wr.write(data)
            if delay is not None:
                time.sleep(delay)
Example #9
0
class MCP9808(object):
    def __init__(self, bus_n, bus_addr=0x18, output='csv'):
        """Initialize worker device on i2c bus.

        Parameters
        ----------
        bus_n : int, i2c bus number on Controller
        bus_addr : int, i2c bus number of this Worker device
        """

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)

        # register values and defaults
        # TODO: do the constant values need to be specified?
        self.reg_map = {
            'config': REG_CONFIG,
            'upper_temp': REG_UPPER_TEMP,
            'lower_temp': REG_LOWER_TEMP,
            'crit_temp': REG_CRIT_TEMP,
            'ambient': REG_AMBIENT_TEMP,
            'manufacturer': REG_MANUF_ID,
            'device_id': REG_DEVICE_ID
        }

        self.alert_critial = None
        self.alert_upper = None
        self.alert_lower = None

        self.manufacturer_id = None
        self.device_id = None
        self.revision = None

        # information about this device
        self.device = DeviceData('MCP9808')
        self.device.description = (
            '+/-0.5 degrees Celcius ' +
            'maximum accuracy digital temperature sensor')
        self.device.urls = 'https://www.microchip.com/datasheet/MCP9808'
        self.device.active = None
        self.device.error = None
        self.device.bus = repr(self.bus)
        self.device.manufacturer = 'Microchip'
        self.device.version_hw = '0.1'
        self.device.version_sw = '0.1'
        self.device.accuracy = '+/-0.25 (typical) C'
        self.device.precision = '0.0625 C maximum'
        self.device.units = 'Degrees Celcius'
        self.device.calibration_date = None

        # data recording information
        self.sample_id = None

        # data recording method
        self.writer_output = output
        self.csv_writer = CSVWriter("MCP9808", time_format='std_time_ms')
        self.csv_writer.device = self.device.__dict__
        self.csv_writer.header = ['description', 'sample_n', 'temperature']

        self.json_writer = JSONWriter("MCP9808", time_format='std_time_ms')
        self.json_writer.device = self.device.__dict__
        self.json_writer.header = ['description', 'sample_n', 'temperature']

    def set_pointer(self, reg_name):
        """Set the pointer register address

        Allowed address names:
            'config'
            'upper_temp'
            'lower_temp'
            'crit_temp'
            'ambient'
            'manufacturer'
            'device_id'

        Parameters
        ----------
        reg_name : str, register address
        """
        reg_addr = self.reg_map[reg_name]

        self.bus.write_byte(reg_addr)

    def read_register_16bit(self, reg_name):
        """Get the values from one registry

        Allowed register names:
            'config'
            'upper_temp'
            'lower_temp'
            'crit_temp'
            'ambient'
            'manufacturer'
            'device_id'

        Parameters
        ----------
        reg_name : str, name of registry to read

        Returns
        -------
        upper byte
        lower byte
        """

        reg_addr = self.reg_map[reg_name]
        ulb = self.bus.read_register_16bit(reg_addr)
        ub = ulb >> 8
        lb = ulb & 0xff
        return ub, lb

    def get_status(self):
        self.get_config()
        self.get_upper_temp()
        self.get_lower_temp()
        self.get_critical_temp()
        self.get_manufacturer()
        self.get_device_id()

    def print_status(self):
        self.get_status()
        print('Configuration Register: {}'.format(None))
        print('Upper Temperature: {}'.format(None))
        print('Lower Temperature: {}'.format(None))
        print('Critical Temperature: {}'.format(None))
        print('Manufacturer: {}'.format(self.manufacturer_id))
        print('Device ID: {}'.format(self.device_id))
        print('Device Revision: {}'.format(self.revision))

    def get_config(self):  #TODO: parse bytes and add docstrings
        ub, lb = self.read_register_16bit('config')

    def get_upper_temp(self):
        ub, lb = self.read_register_16bit('upper_temp')

    def get_lower_temp(self):
        ub, lb = self.read_register_16bit('lower_temp')

    def get_critical_temp(self):
        ub, lb = self.read_register_16bit('crit_temp')

    def get_manufacturer(self):
        ub, lb = self.read_register_16bit('manufacturer')
        self.manufacturer_id = (ub << 8) | lb

    def get_device_id(self):
        (self.device_id, self.revision) = self.read_register_16bit('device_id')

    def get_temp(self):
        """Get temperature in degrees Celcius with 13 bit accuracy

        Returns
        -------
        float, temperature in degrees Celcius
        """

        ub, lb = self.read_register_16bit('ambient')

        self.alert_critial = (ub & 0x80) == 0x80
        self.alert_upper = (ub & 0x40) == 0x40
        self.alert_lower = (ub & 0x20) == 0x20

        ub = ub & 0x1F

        if (ub & 0x10) == 0x10:
            ub = ub & 0x10
            return 256 - (ub * 2**4) + (lb * 2**-4)
        else:
            return (ub * 2**4) + (lb * 2**-4)

    def get(self, description='NA', n=1, delay=None):
        """Get formatted output.

        Parameters
        ----------
        description : char, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        data : list, data containing:
            description: str, description of sample under test
            temperature : float, temperature in degrees Celcius
            delay : float, seconds to delay between samples if n > 1
        """
        data_list = []
        for m in range(1, n + 1):
            data_list.append([description, m, self.get_temp()])
            if n == 1:
                return data_list[0]
            if delay is not None:
                time.sleep(delay)
        return data_list

    def publish(self, description='NA', n=1, delay=None):
        """Output relay status data in JSON.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        str, formatted in JSON with keys:
            description: str, description of sample under test
            temperature : float, temperature in degrees Celcius
        """
        data_list = []
        for m in range(n):
            data_list.append(
                self.json_writer.publish([description, m,
                                          self.get_temp()]))
            if n == 1:
                return data_list[0]
            if delay is not None:
                time.sleep(delay)
        return data_list

    def write(self, description='NA', n=1, delay=None):
        """Format output and save to file, formatted as either
        .csv or .json.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1
        
        Returns
        -------
        None, writes to disk the following data:
            description : str, description of sample
            sample_n : int, sample number in this burst
            temperature : float, temperature in degrees Celcius
        """
        wr = {
            "csv": self.csv_writer,
            "json": self.json_writer
        }[self.writer_output]
        for m in range(n):
            wr.write([description, m, self.get_temp()])
            if delay is not None:
                time.sleep(delay)
Example #10
0
class INA219:
    def __init__(self, bus_n, bus_addr=0x40, output='csv', name='ina219'):
        """Initialize worker device on i2c bus.

        Parameters
        ----------
        bus_n : int, i2c bus number on Controller
        bus_addr : int, i2c bus number of this Worker device
        """

        # i2c bus
        self.bus = base.I2C(bus_n=bus_n, bus_addr=bus_addr)

        self.reg_config = None
        self.reg_shunt_voltage = None
        self.reg_bus_voltage = None
        self.reg_power = None
        self.reg_calibration = None

        self.reg_map = {
            'config': 0x00,
            'shunt_voltage': 0x01,
            'bus_voltage': 0x02,
            'power': 0x03,
            'current': 0x04,
            'calibration': 0x05
        }

        # bus voltage range
        self.bv_reg_to_bv = {0: 16, 1: 32}

        # programable gain amplifier
        self.pga_reg_to_gain = {0: 1, 1: 2, 2: 4, 3: 8}
        self.pga_gain_to_reg = {1: 0, 2: 1, 4: 2, 8: 3}
        self.pga_reg_str_range = {
            1: "+/- 40 mV",
            2: "+/- 80 mV",
            4: "+/- 160 mV",
            8: "+/- 320 mV"
        }

        # print debug statements
        self.verbose = False

        # data recording information
        self.sample_id = None

        # information about this device
        self.metadata = Meta(name=name)
        self.metadata.description = 'Texas Instruments Bidirectional Current Monitor'
        self.metadata.urls = 'www.ti.com/product/ADS1115'
        self.metadata.manufacturer = 'Adafruit Industries & Texas Instruments'

        self.metadata.header = [
            'description', 'sample_n', 'voltage', 'current'
        ]
        self.metadata.dtype = ['str', 'int', 'float', 'float']
        self.metadata.units = [None, 'count', 'volt', 'amp']
        self.metadata.accuracy = [None, 1, '+/-0.2%', '+/-0.2%']
        self.metadata.precision = [None, 1, '4 mV', '10uV accross shunt']
        self.metadata.accuracy_note = 'values for model INA291A'

        self.metadata.bus_n = bus_n
        self.metadata.bus_addr = hex(bus_addr)

        # chip defaults on power up or reset command
        self.metadata.bus_voltage_range = self.bv_reg_to_bv[1]
        self.metadata.gain = self.pga_reg_to_gain[0b11]
        self.metadata.gain_string = self.pga_reg_str_range[self.metadata.gain]

        self.metadata.bus_adc_resolution = 12
        self.metadata.bus_adc_averaging = None

        self.metadata.shunt_adc_resolution = 12
        self.metadata.shunt_adc_averaging = None

        self.metadata.mode = 7
        self.mode_to_str = {
            0: "power down",
            1: "shunt voltage, triggered",
            2: "bus voltage, triggered",
            3: "shunt and bus voltages, triggered",
            4: "ADC off (disabled)",
            5: "shunt voltage, continuous",
            6: "bus voltage, continuous",
            7: "shunt and bus voltages, continuous"
        }
        self.metadata.mode_description = self.mode_to_str[self.metadata.mode]

        # Adafruit INA219 breakout board as a 0.1 ohm 1% 2W resistor
        self.metadata.r_shunt = 0.1

        # data recording method
        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata,
                                    time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata,
                                      time_format='std_time_ms')

        # intialized configuration values
        self.get_config()

    def read_register(self, reg_name):
        """Get the values from one registry

        Allowed register names:
            'config'
            'shunt_voltage'
            'bus_voltage'
            'power'
            'current'
            'calibration'

        Parameters
        ----------
        reg_name : str, name of registry to read

        Returns
        -------
        16 bit registry value
        """

        reg_addr = self.reg_map[reg_name]
        return self.bus.read_register_16bit(reg_addr)

    def get_config(self):
        r = self.read_register('config')
        self.reg_config = r
        self.metadata.bus_voltage_range = self.bv_reg_to_bv[(r >> 13) & 0b1]
        self.metadata.gain = self.pga_reg_to_gain[(r >> 11) & 0b11]

        if self.verbose:
            print("Bus Voltage Range:", self.metadata.bus_voltage_range, "V")
            print("PGA Range: {}x or {}".format(
                self.metadata.gain,
                self.pga_reg_str_range[self.metadata.gain]))
            print("Configuration Register:")
            tools.bprint(r)
        return r

    def get_shunt_voltage(self):
        """Read the shunt voltage register where LSB = 10uV

        Note: datasheet calculations result in mV, see section 8.5.1
        
        Returns
        -------
        float : shunt voltage in volts (not millivolts)
        """
        return self.read_register('shunt_voltage') * 0.00001

    def get_bus_voltage(self):
        """Read the bus voltage register where LSB = 4mV.
        
        Note: Right most 3 bits of bus voltage register are 0, CNVR, OVF
              32 volt range => 0 to 32 VDC
              16 volt range => 0 to 16 VDC
        
        Returns
        -------
        float : bus voltage in volts (not millivolts)
        """
        r = self.read_register('bus_voltage')
        return (r >> 3) * 4.0 * 0.001

    def get_power(self):
        r = self.read_register('power')
        return r

    def get_current(self):
        r = self.read_register('current')
        return r

    def get_calibration(self):
        r = self.read_register('calibration')
        return r

    def write_register(self, reg_name, data):
        """Write a 16 bits of data to register

        Allowed register names:
            'config'
            'shunt_voltage'
            'bus_voltage'
            'power'
            'current'
            'calibration'

        Parameters
        ----------
        reg_name : str, name of registry to read
        data : int, 16 bit value to write to register
        """

        reg_addr = self.reg_map[reg_name]
        if self.verbose:
            print("Writing to '{}' registry # {}".format(reg_name, reg_addr))
            tools.bprint(data)
        self.bus.write_register_16bit(reg_addr, data)

    def write_config(self, data):
        self.write_register('config', data)

    def write_calibration(self, data):
        self.write_register('calibration', data)

    def reset(self):
        self.write_config(self.reg_config | 0b1000000000000000)

    def set_bus_voltage_range(self, v=32):
        reg_value = {
            16: base.bit_clear(13, self.reg_config),
            32: base.bit_set(13, self.reg_config)
        }[v]
        self.reg_config = reg_value
        self.metadata.bus_voltage_range = v
        self.write_config(reg_value)

    def set_pga_range(self, gain=8):
        mask = 0b1110011111111111  #                 PGA  10
        reg_value = {
            1: (self.reg_config & mask) | 0b0000000000000000,
            2: (self.reg_config & mask) | 0b0000100000000000,
            4: (self.reg_config & mask) | 0b0001000000000000,
            8: (self.reg_config & mask) | 0b0001100000000000
        }[gain]
        self.reg_config = reg_value
        self.metadata.gain = gain
        self.metadata.gain_string = self.pga_reg_str_range[self.gain]
        self.write_config(reg_value)

    def set_bus_adc_resolution(self, bits=12):
        """Generate config register for setting ADC resolution"""
        mask = 0b1111100001111111  #                 BADC   4321
        reg_value = {
            9: (self.reg_config & mask) | 0b0000000000000000,
            10: (self.reg_config & mask) | 0b0000000010000000,
            11: (self.reg_config & mask) | 0b0000000100000000,
            12: (self.reg_config & mask) | 0b0000000110000000
        }[bits]
        self.reg_config = reg_value
        self.metadata.bus_adc_resolution = bits
        self.metadata.bus_adc_averaging = None
        self.write_config(reg_value)

    def set_bus_adc_samples(self, n=128):
        """Generate config register for setting ADC sample averaging"""
        mask = 0b1111100001111111  #                 BADC   4321
        reg_value = {
            2: (self.reg_config & mask) | 0b0000010010000000,
            4: (self.reg_config & mask) | 0b0000010100000000,
            8: (self.reg_config & mask) | 0b0000010110000000,
            16: (self.reg_config & mask) | 0b0000011000000000,
            32: (self.reg_config & mask) | 0b0000011010000000,
            64: (self.reg_config & mask) | 0b0000011100000000,
            128: (self.reg_config & mask) | 0b0000011110000000
        }[n]
        self.reg_config = reg_value
        self.metadata.bus_adc_resolution = None
        self.metadata.bus_adc_averaging = n
        self.write_config(reg_value)

    def set_shunt_adc_resolution(self, bits=12):
        """Generate config register for setting ADC resolution"""
        mask = 0b1111111110000111  #                 SADC       4321
        reg_value = {
            9: (self.reg_config & mask) | 0b0000000000000000,
            10: (self.reg_config & mask) | 0b0000000000001000,
            11: (self.reg_config & mask) | 0b0000000000010000,
            12: (self.reg_config & mask) | 0b0000000000011000
        }[bits]
        self.reg_config = reg_value
        self.metadata.shunt_adc_resolution = bits
        self.metadata.shunt_adc_averaging = None
        self.write_config(reg_value)

    def set_shunt_adc_samples(self, n=128):
        mask = 0b1111111110000111  #                 SADC       4321
        reg_value = {
            2: (self.reg_config & mask) | 0b0000000001001000,
            4: (self.reg_config & mask) | 0b0000000001010000,
            8: (self.reg_config & mask) | 0b0000000001011000,
            16: (self.reg_config & mask) | 0b0000000001100000,
            32: (self.reg_config & mask) | 0b0000000001101000,
            64: (self.reg_config & mask) | 0b0000000001110000,
            128: (self.reg_config & mask) | 0b0000000001111000
        }[n]
        self.reg_config = reg_value
        self.metadata.adc_resolution = None
        self.metadata.adc_averaging = n
        self.write_config(reg_value)

    def set_mode(self, n=7):
        mask = 0b1111111111111000  #                 MODE           321
        reg_value = {
            0: (self.reg_config & mask) | 0b0000000000000000,
            1: (self.reg_config & mask) | 0b0000000000000001,
            2: (self.reg_config & mask) | 0b0000000000000010,
            3: (self.reg_config & mask) | 0b0000000000000011,
            4: (self.reg_config & mask) | 0b0000000000000100,
            5: (self.reg_config & mask) | 0b0000000000000110,
            6: (self.reg_config & mask) | 0b0000000000000111
        }[n]
        self.reg_config = reg_value
        self.metadata.mode = n
        self.metadata.mode_description = self.mode_to_str[n]
        self.write_config(reg_value)

    def set_calibration(self, cal_value):
        """Set the calibration register, see datasheet for details

        cal_value : int, register value to write
        """
        self.reg_calibration = cal_value
        self.write_calibration(cal_value)

    def get_current_simple(self):
        """Calculate the current from single shot measurements"""
        return self.get_shunt_voltage() / self.metadata.r_shunt

    def get(self, description='NA', n=1, delay=None):
        """Get formatted output.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1
        
        Returns
        -------
        data : list, data that will be saved to disk with self.write containing:
            description: str, description of sample under test
            sample_n : int, sample number in this burst
            voltage, float, Volts measured at the shunt resistor
            current : float, Amps of current accross the shunt resistor
        """
        data_list = []
        for m in range(1, n + 1):
            data_list.append([
                description, m,
                self.get_bus_voltage(),
                self.get_current_simple()
            ])
            if n == 1:
                return data_list[0]
            if delay is not None:
                time.sleep(delay)
        return data_list

    def publish(self, description='NA', n=1, delay=None):
        """Output relay status data in JSON.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        str, formatted in JSON with keys:
            description: str, description of sample under test
            sample_n : int, sample number in this burst
            voltage, float, Volts measured at the shunt resistor
            current : float, Amps of current accross the shunt resistor
        """
        data_list = []
        for m in range(n):
            data_list.append(
                self.json_writer.publish([
                    description, m,
                    self.get_bus_voltage(),
                    self.get_current_simple()
                ]))
            if n == 1:
                return data_list[0]
            if delay is not None:
                time.sleep(delay)
        return data_list

    def write(self, description='NA', n=1, delay=None):
        """Format output and save to file, formatted as either .csv or .json.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        None, writes to disk the following data:
            description : str, description of sample
            sample_n : int, sample number in this burst
            voltage, float, Volts measured at the shunt resistor
            current : float, Amps of current accross the shunt resistor
        """
        wr = {
            "csv": self.csv_writer,
            "json": self.json_writer
        }[self.writer_output]
        for m in range(n):
            wr.write([
                description, m,
                self.get_bus_voltage(),
                self.get_current_simple()
            ])
            if delay is not None:
                time.sleep(delay)
Example #11
0
class Atlas:
    """Base class for Atlas Scientific sensors"""


    def __init__(self, bus_n, bus_addr, output='csv'):
        """Initialize worker device on i2c bus.

        Parameters
        ----------
        bus_n : int, i2c bus number on Controller
        bus_addr : int, i2c bus number of this Worker device
        """

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)


        # time to wait for conversions to finish
        self.short_delay   = 0.3  # seconds for regular commands
        self.long_delay    = 1.5  # seconds for readings
        self.cal_delay     = 3.0  # seconds for calibrations

        # information about this device
        self.device = DeviceData('Atlas_Base')
        self.device.description = ('')
        self.device.urls = 'www.atlas-scientific.com'
        self.device.active = None
        self.device.error = None
        self.device.bus = repr(self.bus)
        self.device.manufacturer = 'Atlas Scientific'
        self.device.version_hw = '1.0'
        self.device.version_sw = '1.0'
        self.device.accuracy = None
        self.device.precision = 'Varies'
        self.device.calibration_date = None

        """
        # data recording method
        if output == 'csv':
            self.writer = CSVWriter('Atlas_Base', time_format='std_time_ms')

        elif output == 'json':
            self.writer = JSONWriter('Atlas_Base', time_format='std_time_ms')
        else:
            pass  # holder for another writer or change in default
        self.writer.header = ['description', 'sample_n', 'not_set']
        self.writer.device = self.device.values()
        """

        # data recording information
        self.sample_id = None

        # data recording method
        self.writer_output = output
        self.csv_writer = CSVWriter("Atlas_Base", time_format='std_time_ms')
        self.csv_writer.device = self.device.__dict__
        self.csv_writer.header = ['description', 'sample_n', 'not_set']

        self.json_writer = JSONWriter("Atlas_Base", time_format='std_time_ms')
        self.json_writer.device = self.device.__dict__
        self.json_writer.header = self.csv_writer.header

    def query(self, command, n=31, delay=None, verbose=False):
        """Write a command to the i2c device and read the reply,
        delay between reply based on command value.

        Byte codes:

            First byte repsonse codes
                  0x1 = successful request
                  0x2 = syntax error
                0x254 = still processing, not ready
                0x255 = no data to send
            Filler byte
                  0x0 = filler (usually at the end of a reply)

        Parameters
        ----------
        command : str, command to execute on the device
        n : int, number of bytes to read
        delay : float, number of milliseconds to delay before reading response
        verbose : bool, print debug statements

        Returns
        -------
        str : response, may require further parsing
        """

        if verbose:
            print("Input Type: ", type(command))

        if (type(command) == int) or (type(command) == bytes):
            byte_command = command
        else:
            byte_command = [ord(x) for x in command]

        if verbose:
            print("Sent:", command)

        self.bus.write_n_bytes(*[byte_command])

        if delay is not None:
            time.sleep(delay/1000)

        if n != 0:
            reply = self.bus.read_n_bytes(n=n)

            reply_mapper = {0: 'Filler',
                            1: 'Success',
                            254: 'Still processing',
                            255: 'No data'}

            if verbose:
                # print the response code that is in position 0 of reply
                reply_0 = reply[0]
                print("Reply:", reply_mapper[reply_0])

            # filter out response codes and filler bytes
            reply_bytes = bytearray([])
            for reply_n in reply:
                if reply_n not in [0x0, 0x1, 0x2, 0x254, 0x255]:
                    reply_bytes.append(reply_n)
            reply_bytes = bytes(reply_bytes)
            reply_bytes = reply_bytes.decode('utf-8')

            if verbose:
                print("Formatted and trimmed reply:", reply_bytes)

            return reply_bytes

    def led_on(self):
        """Turn on status LED until another character is set"""
        self.query(b'L,1', n=0)

    def led_off(self):
        """Turn off status LED"""
        self.query(b'L,0', n=0)

    def find_start(self):
        """Blink white LED until another character is set"""
        self.query(b'Find', n=0)

    def find_stop(self):
        """Stop white LED from blinking"""
        self.query(0x01, n=0)

    def info(self):
        """Get device information

        Returns
        -------
        device: str, device type
        firmware : str, firmware version
        """

        _r = self.query(b'i', n=15, delay=350)
        _, device, firmware = _r.split(",")
        return device, firmware

    def status(self):
        """Get device status

        Returns
        -------
        restart code : str, one character meaning
            P = power off
            S = software reset
            B = brown out
            W = watchdog
            U = unknown
        vcc : float, supply voltage of input to device
        """
        _r = self.query(b'Status', n=15, delay=350)
        _, restart_code, vcc = _r.split(",")
        return restart_code, float(vcc)

    def sleep(self):
        """Put device to sleep.  Any byte sent wakes."""
        _r = self.query(b'Sleep', n=0)

    def wake(self):
        """Wake device from sleep state"""
        _r = self.query(0x01, n=0)

    def plock_status(self):
        """Get protocol lock status"""
        _r = self.query(b'Plock,?', n=9, delay=350, verbose=verbose)
        _, plock_state = _r.split(",")
        return int(plock_state)

    def plock_on(self):
        """Lock device into I2C mode"""
        _r = self.query(b'Plock,1', n=0)

    def plock_off(self):
        """Unlock device from I2C mode"""
        _r = self.query(b'Plock,0', n=0)

    def reset(self):
        """Completely reset device.  Clears calibration, sets LED on and
        enables reponse codes"""
        _r = self.query(b'Factory', n=0)

    def change_i2c_bus_address(self, n):
        """Change the device I2C bus address - device will not be accessible
        until contacted at new I2C address.  Response is device reboot.

        Parameters
        ----------
        n : int, I2C address, can be any number 1-127 inclusive
        """
        _r = self.query(bytes("I2C,{}".format(n), encoding='utf-8'), n=0)

    def get(self, description='no_description', n=1, delay=0):
        """Get formatted output, assumes subclass has method 'measure'

        Parameters
        ----------
        description : char, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        data : list, data that will be saved to disk with self.write containing:
            description : str
            c : float, sensor measurement
        """

        data_list = []
        for m in range(n):
            measure = self.measure()
            if isinstance(measure, float):
                measure = [measure]
            data = [description, m] + measure
            data_list.append(data)
            if n == 1:
                return data_list[0]
            time.sleep(max(self.long_delay, delay))
        return data_list

    def publish(self, description='NA', n=1, delay=0):
        """Output relay status data in JSON.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        str, formatted in JSON with keys:
            description: str, description of sample under test
            measurement : float, measurement made
        """
        data_list = []
        for m in range(n):
            measure = self.measure()
            if isinstance(measure, float):
                measure = [measure]
            data_list.append(self.json_writer.publish([description, m] + measure))
            if n == 1:
                return data_list[0]
            time.sleep(max(self.long_delay, delay))
        return data_list

    def write(self, description='NA', n=1, delay=0):
        """Format output and save to file, formatted as either .csv or .json.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        None, writes to disk the following data:
            description : str, description of sample
            sample_n : int, sample number in this burst
            voltage, float, Volts measured at the shunt resistor
            current : float, Amps of current accross the shunt resistor
        """
        wr = {"csv": self.csv_writer,
              "json": self.json_writer}[self.writer_output]
        for m in range(n):
            measure = self.measure()
            if ((isinstance(measure, float)) or 
                (isinstance(measure, int)) or 
                (isinstance(measure, str))):
                    measure = [measure]
            wr.write([description, m] + measure)
            time.sleep(max(self.long_delay, delay))
Example #12
0
class PA1010D(Base):
    def __init__(self, bus_n, bus_addr=0x10, output='csv', name='pa1010d'):
        """PA1010D GPS module using MTK3333 chipset
        
        Supported NMEA sentences: 'GGA', 'GSA', 'GSV', 'RMC', 'VTG'
        
        Parameters
        ----------
        bus_n : int, i2c bus number on Controller
        bus_addr : int, i2c bus number of this Worker device
        nmea_sentence : str, NMEA sentence type to save for CSV.
            JSON will save
        """

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)

        # maximum data in buffer size
        self._bytes_per_burst = 255
        
        # standard metadata information about this device
        self.metadata = Meta(name=name)
        
        self.metadata.description      = 'Adafruit PA1010D GPS/GNSS module'
        self.metadata.urls             = 'https://www.cdtop-tech.com/products/pa1010d'
        self.metadata.manufacturer     = 'CDTop Technology'
        self.metadata.state            = None
        
        self.metadata.header    = ['description', 'sample_n', 'nmea_sentence']
        self.metadata.dtype     = ['str', 'int', 'str']
        self.metadata.units     = None
        self.metadata.accuracy  = None
        self.metadata.precision = '<3.0 meters'
        
        self.metadata.bus_n            = bus_n
        self.metadata.bus_addr         = hex(bus_addr)
        
        # custom metadata attributes
        self.metadata.supported_nmea_sentences = ['GGA', 'GSA', 'GSV', 'RMC', 'VTG']
        
        # data recording method
        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata,
                                    time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata,
                                      time_format='std_time_ms')

    def raw_get(self):
        """Get byte data from the GPS module, filtered of blanks
        and combining lines"""
        
        data = ""
        _d = self.bus.read_n_bytes(n=self._bytes_per_burst)
        _d = _d.decode("UTF-8")
        data = "".join([data, _d])

        data = regex.split(data)
        data = [_d for _d in data if _d != ""]
        data = set(data)

        data_out = []
        last_line = ""
        started = False

        for line in data:
            if (line[0] == "$") & ("*" in line):
                data_out.append(line)
            elif ("$" in line):
                last_line = line
            elif ("*" in line) & (last_line != ""):
                data_out.append(last_line + line)

        return data_out
        
    def get(self, nmea_sentences=None, timeout=15):
        """Get NMEA sentences
        
        Parameters
        ----------
        nmea_sentence : list of str, NMEA sentence codes to return i.e. 'GSV'
        timeout : int, seconds to wait for data before returning
        
        Returns
        -------
        list of str, NMEA sentences
        """
        
        if nmea_sentences is None:
            nmea_sentences = self.metadata.supported_nmea_sentences
        
        check = {s: False for s in nmea_sentences}
        
        t0 = time.time()
        while sum([x == False for x in check.values()]) > 0:
            if time.time() - t0 > timeout:  # timeout in seconds
                break
            
            for single_line in self.raw_get():
                sentence_type = single_line[3:6]
                if (sentence_type in nmea_sentences) & ("*" in single_line):
                    gps_data = single_line.split("*")
                    gps_cs = gps_data[1]
                    gps_data = gps_data[0][1:]
                    driver_cs = calc_checksum(gps_data)
                    if gps_cs == driver_cs:
                        check[sentence_type] = single_line
                        
        return [v for k, v in check.items()]
        
    def send_command(self, command, add_format=True):
        """Send a command string to the GPS.
        
        Note: If add_format=True, do not add leading 
        '$' and trailing '*' plus checksum, these will 
        be added automatically
        
        Parameters
        ----------
        command : str, command to send
        add_format : bool, add '$' prefix and '*' plus calculated checksum
        """
        if add_format:
            checksum = calc_checksum(command)
            command = "${}*{}\r\n".format(command, checksum)
        command = bytes(command)
        self.bus.write_n_bytes(command)
        
    def full_power_mode(self):
        """Enable full power continuous mode"""
        self.send_command(b"$PMTK225,0*2B", add_format=False)
        
    def standby_mode(self):
        """Enable standby power mode"""
        self.send_command(b"$PMTK161,0*28", add_format=False)
        
    def periodic_mode(self, mode_type, rt1, st1, rt2, st2):
        """Enable periodic power on/off mode
        
        Parameters
        ----------
        mode_type : int, 1 or 2 where:
            1 = periodic backup mode
            2 = periodic standby mode
            See other mode method for descriptions
        rt1 : int, full power mode length in milliseconds
        st1 : int, mode_type length in milliseconds
        rt2 : int, full power mode length in milliseconds
        st2 : int, mode_type length in milliseconds
        """
        assert mode_type in [1,2]
        
        command = "PMTK225{},{},{},{},{}".format(mode_type, rt1, st1, rt2, st2)
        command = bytes(command)
        self.send_command(command, add_format=True)
        
    def always_locate_mode(self):
        """Enable always locate mode"""
        self.send_command(b"$PMTK225,8*23", add_format=False)
        
    def backup_mode(self):
        """Enable backup mode 
        Note: Before sending the command the WAKE-UP pin (pin 2) must 
        be tied to ground. It is not possible to wake up the module 
        from backup mode by software command."""
        self.send_command(b"$PMTK225,4*2F", add_format=False)
        
    def publish(self, description='NA', n=1, nmea_sentences=None, delay=None):
        """Output relay status data in JSON.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        nmea_sentence : list of str, NMEA sentence codes to return i.e. 'GSV'
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        str, formatted in JSON with keys:
            description : str
            n : sample number in this burst
            nmea_sentence : str, NMEA sentence
        """
        if nmea_sentences is None:
            nmea_sentences = self.metadata.supported_nmea_sentences

        data_list = []
        for m in range(n):
            nmea_data = self.get(nmea_sentences=nmea_sentences)
            for p in range(len(nmea_data)):
                data = self.json_writer.publish([description, m, nmea_data[p]])
                data_list.append(data)
            if delay is not None:
                time.sleep(delay)
        return data_list
        
    def write(self, description='NA', n=1, nmea_sentences=None, delay=None):
        """Format output and save to file, formatted as either .csv or .json.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        nmea_sentence : list of str, NMEA sentence codes to return i.e. 'GSV'
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        None, writes to disk the following data:
            description : str
            n : sample number in this burst
            nmea_sentence : str, NMEA sentence
        """ 
        if nmea_sentences is None:
            nmea_sentences = self.metadata.supported_nmea_sentences
        
        wr = {"csv": self.csv_writer,
              "json": self.json_writer}[self.writer_output]
        for m in range(n):
            nmea_data = self.get(nmea_sentences=nmea_sentences)
            for p in range(len(nmea_data)):
                wr.write([description, m, '"' + nmea_data[p] + '"'])
            if delay is not None:
                time.sleep(delay)
Example #13
0
class ADS1115(object):
    def __init__(self, bus_n, bus_addr=0x48, output='csv', name='ADS1115'):
        """Initialize worker device on i2c bus.

        Parameters
        ----------
        bus_n : int, i2c bus number on Controller
        bus_addr : int, i2c bus number of this Worker device
        """

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)

        # time to wait for conversion to finish
        self.delay = 0.009  # units = seconds

        # register values and defaults
        self.conversion_value = 40000  # higher than any conversion result
        self.config_value = None  # 0x8583 default
        self.lo_thres_value = None  # 0x8000 default
        self.hi_thres_value = None  # 0x7fff default

        self.reg_map = {
            'conversion': 0b00,
            'config': 0b01,
            'lo_thresh': 0b10,
            'hi_thresh': 0b11
        }

        # config register attributes and chip defaults
        self.comp_que_value = 0b11
        self.comp_lat_value = 0b0
        self.comp_pol_value = 0b0
        self.comp_mode_value = 0b0
        self.dr_value = 0b100
        self.mode_value = 0b1
        self.pga_value = 0b010
        self.mux_value = 0b000
        self.os_value = 0b0

        # voltage measurement
        self.pga_float = -999
        self.volts = -999

        # attribute converters
        self.str_mux = {
            '01': 0b000,
            '03': 0b001,
            '13': 0b010,
            '23': 0b011,
            '0G': 0b100,
            '1G': 0b101,
            '2G': 0b110,
            '3G': 0b111
        }
        self.bin_mux = {v: k for k, v in self.str_mux.items()}

        self.str_pga = {
            '6.144': 0b000,
            '4.096': 0b001,
            '2.048': 0b010,
            '1.024': 0b011,
            '0.512': 0b100,
            '0.256': 0b101
        }
        self.bin_pga = {v: float(k) for k, v in self.str_pga.items()}
        self.str_mode = {'continuous': 0b0, 'single': 0b1}
        self.bin_mode = {v: k for k, v in self.str_mode.items()}
        self.str_data_rate = {
            8: 0b000,
            16: 0b001,
            32: 0b010,
            64: 0b011,
            128: 0b100,
            250: 0b101,
            475: 0b110,
            860: 0b111
        }
        self.bin_data_rate = {v: k for k, v in self.str_data_rate.items()}
        self.str_comp_mode = {'trad': 0b0, 'window': 0b1}
        self.bin_comp_mode = {v: k for k, v in self.str_comp_mode.items()}
        self.str_comp_pol = {'1': 0b00, '2': 0b01, '3': 0b10, 'off': 0b11}
        self.bin_comp_pol = {v: k for k, v in self.str_comp_pol.items()}
        self.str_comp_lat = {'off': 0b0, 'on': 0b1}
        self.bin_comp_lat = {v: k for k, v in self.str_comp_lat.items()}
        self.str_comp_que = {'1': 0b00, '2': 0b01, '3': 0b10, 'off': 0b11}
        self.bin_comp_que = {v: k for k, v in self.str_comp_que.items()}

        # information about this device
        self.metadata = Meta(name=name)
        self.metadata.description = ('Texas Instruments 16-bit 860SPS' +
                                     ' 4-Ch Delta-Sigma ADC with PGA')
        self.metadata.urls = 'www.ti.com/product/ADS1115'
        self.metadata.manufacturer = 'Texas Instruments'

        self.metadata.header = ['description', 'sample_n', 'mux', 'voltage']
        self.metadata.dtype = ['str', 'int', 'str', 'float']
        self.metadata.units = [None, 'count', 'str', 'volts']
        self.metadata.accuracy = [None, 1, None, '+/- 3 LSB']
        self.metadata.precision = [None, 1, None, '16 bit']

        self.metadata.bus_n = bus_n
        self.metadata.bus_addr = hex(bus_addr)

        # current settings of this device
        self.metadata.pga_gain = self.pga_float

        # data recording information
        self.sample_id = None

        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata,
                                    time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata,
                                      time_format='std_time_ms')

        # initialize class attributes from device registry
        self.get_config()

    def twos_comp_to_dec(self, value, bits):
        """Convert Two's Compliment format to decimal"""
        if (value & (1 << (bits - 1))) != 0:
            value = value - (1 << bits)
        return value

    def set_pointer(self, reg_name):
        """Set the pointer register address

        Allowed register names:
            'conversion'
            'config'
            'lo_thres'
            'hi_thresh'

        Parameters
        ----------
        reg_name : str, register address
        """
        reg_addr = self.reg_map[reg_name]

        self.bus.write_byte(reg_addr)

    def read_register_16bit(self, reg_name):
        """Get the values from one registry

        Allowed register names:
            'conversion'
            'config'
            'lo_thres'
            'hi_thresh'

        Parameters
        ----------
        reg_name : str, name of registry to read

        Returns
        -------
        16 bit registry value
        """

        reg_addr = self.reg_map[reg_name]
        return self.bus.read_register_16bit(reg_addr)

    def write_register_16bit(self, reg_name, data):
        """Write a 16 bits of data to register

        Allowed register names:
            'conversion'
            'config'
            'lo_thres'
            'hi_thresh'

        Parameters
        ----------
        reg_name : str, name of registry to read
        data : int, 16 bit value to write to register
        """

        reg_addr = self.reg_map[reg_name]
        self.bus.write_register_16bit(reg_addr, data)
        return True

    def set_config(self):
        self.write_register_16bit('config', self.config_value)
        return True

    def get_conversion(self):
        """Read the ADC conversion register at address 0x00
        Default value on power up from chip is 0
        """

        self.conversion_value = self.read_register_16bit('conversion')
        return self.conversion_value

    def get_config(self):
        """Read the configuration register at address 0x01
        Default value from chip is 0x8583 = 34179 = '0b1000010110000110'
        Specific states extracted with other methods
        """

        self.config_value = self.read_register_16bit('config')
        self.update_attributes()

    def update_attributes(self):
        """Update all attributes
        TODO: add more detail
        """
        # Comparator queue and disable
        self.comp_que_value = self.config_value & 0b11
        # Latching comparator
        self.comp_lat_value = (self.config_value >> 2) & 0b1
        # Comparator polarity
        self.comp_pol_value = (self.config_value >> 3) & 0b1
        # Comparator mode
        self.comp_mode_value = (self.config_value >> 4) & 0b1
        # Data rate
        self.dr_value = (self.config_value >> 5) & 0b111
        # Operating mode
        self.mode_value = (self.config_value >> 8) & 0b1
        # Programmable gain amplifier
        self.pga_value = (self.config_value >> 9) & 0b111
        self.pga_float = self.bin_pga[self.pga_value]
        # Multiplexer
        self.mux_value = (self.config_value >> 12) & 0b111
        # Operational status / Single-shot conversion start
        self.os_value = self.config_value >> 15

    def get_lo(self):
        """Read the low threshold register at address 0x02
        Default value from chip is 0x8000
        """

        self.lo_thres_value = self.read_register_16bit('lo_thresh')
        return self.lo_thres_value

    def get_hi(self):
        """Read the high threshold register at address 0x03
        Default value from chip is 0x7FFF
        """

        self.hi_thres_value = self.read_register_16bit('hi_thresh')
        return self.hi_thres_value

    def os(self):
        """Set the operational status
        As this has only one use in write mode, sets bit 15 to True.
        Chip will automatically clear it.  Bit 15 = True is also the
        power on default - it should be sufficient to read the configuration
        register once and use that bit position for all read commands.
        """
        self.config_value = (self.config_value & BIT_OS) | (0b1 << 15)
        self.set_config()
        self.os_value = self.config_value >> 15

    def mux(self, x):
        """Set multiplexer pin pair, ADS1115 only.

        Parameters
        ----------
        x : str, positive and negative pin combination.  Based on:
            AIN pins '0', '1', '2', '3' and Ground pin 'G'
            i.e. for AIN_pos = AIN0 and AIN_neg = Ground, x = '0G'
        """

        self.config_value = ((self.config_value & BIT_MUX)
                             | (self.str_mux[x] << 12))
        self.set_config()
        time.sleep(self.delay)
        self.mux_value = (self.config_value >> 12) & 0b111

    def pga(self, x):
        """Set programmable gain amplifier range.

        Parameters
        ----------
        x : str, +/- voltage range value.  Supported values:
            '6.144', '4.096', '2.048', '1.024', '0.512', '0.256'
        """

        self.config_value = ((self.config_value & BIT_PGA)
                             | (self.str_pga[x] << 9))
        self.set_config()
        time.sleep(self.delay)  # needs at least 7 ms to complete
        self.pga_value = (self.config_value >> 9) & 0b111
        self.pga_float = self.bin_pga[self.pga_value]

    def mode(self, x):
        """Set operating mode to either single or continuous.

        Parameters
        ----------
        x: str, either 'single' or 'continuous'
        """

        self.config_value = ((self.config_value & BIT_MODE)
                             | (self.str_mode[x] << 8))
        self.set_config()
        time.sleep(self.delay)
        self.mode_value = (self.config_value >> 8) & 0b1

    def data_rate(self, x):
        """Set data rate of sampling
        Changes bits [7:5]

        Parameters
        ----------
        x : int, samples per second.
            Allowed values: 8, 16, 32, 64,
            128, 250, 475, 860
        """

        self.config_value = ((self.config_value & BIT_DR)
                             | (self.str_data_rate[x] << 5))
        self.set_config()
        self.dr_value = (self.config_value >> 5) & 0b111

    def comp_mode(self, x):
        """Set comparator mode
        ADS1114 and ADS1115 only, changes bit 4

        x : str, 'trad' or 'window'
        """

        self.config_value = ((self.config_value & BIT_COMP_MODE)
                             | (self.str_comp_mode[x] << 4))
        self.set_config()
        self.comp_mode_value = (self.config_value >> 4) & 0b1

    def comp_polarity(self, x):
        """Set polarity of ALERT/RDY pin when active.
        No function in ADS1113, changes bit 3

        Parameters
        ----------
        x : str, 'high' or 'low'
        """

        self.config_value = ((self.config_value & BIT_COMP_POL)
                             | (self.str_comp_pol[x] << 3))
        self.set_config()
        self.comp_pol_value = (self.config_value >> 3) & 0b1

    def comp_latching(self, x):
        """Set whether the ALERT/RDY pin latches or clears when conversions
        are within the margins of the upper and lower thresholds

        Only available in ADS1114 and ADS1115, default is 0 = non-latching

        Parameters
        ----------
        x : str, 'on' or 'off'
        """

        self.config_value = ((self.config_value & BIT_COMP_LAT)
                             | (self.str_comp_lat[x] << 2))
        self.set_config()
        self.comp_lat_value = (self.config_value >> 2) & 0b1

    def comp_que(self, x):
        """Disable or set the number of conversions before a ALERT/RDY pin
        is set high

        Parameters
        ----------
        x : str, number of conversions '1', '2', '4' or 'off'
        """

        self.config_value = ((self.config_value & BIT_COMP_QUE)
                             | (self.str_comp_lat[x] << 0))
        self.set_config()
        self.comp_que_value = self.config_value & 0b11

    def single_shot(self):
        """Write 0x1 to bit 15 of the configuration register to initialize
        a single shot conversion.  The configuration register must be read at
        least once to get the current configuration, otherwise the chip default is used.
        Chip clears bit on completion of ADC conversion.
        """

        self.os()
        time.sleep(self.delay)
        self.get_conversion()

    def voltage(self):
        """Calculate the voltage measured by the chip based on conversion
        register and configuration register values
        """

        if self.mode_value == 1:
            self.single_shot()
        else:
            self.get_conversion()
        _x = self.twos_comp_to_dec(self.conversion_value, 16)
        self.volts = _x * (self.pga_float / 2**15)
        return self.volts

    def print_attributes(self):
        """Print to console current attributes"""
        print('ADS11x5 Configuration Attributes')
        print('--------------------------------')
        print('Config Register:', self.config_value, hex(self.config_value),
              bin(self.config_value))
        print('PGA Range: +/-', self.pga_float, 'Volts')
        print('Mode:', self.bin_mode[self.mode_value])
        print('Data Rate:', self.bin_data_rate[self.dr_value], 'SPS')
        print('Input Multiplexer:', self.bin_mux[self.mux_value])
        print('Comparator:')
        print(' Queue:', self.bin_comp_que[self.comp_que_value])
        print(' Latching:', self.bin_comp_lat[self.comp_lat_value])
        print(' Polarity: Active', self.bin_comp_pol[self.comp_pol_value])
        print(' Mode:', self.bin_comp_mode[self.comp_mode_value])

    def get(self, description='no_description', n=1, delay=None):
        """Get ADC data.

        Parameters
        ----------
        description : char, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        data : list, data that will be saved to disk with self.write containing:
            description : str
            n : sample number in this burst
            mux : XXX, multiplexer pin pair the voltage reading was taken with
            v : float, voltage measurement
        """
        data_list = []
        for m in range(1, n + 1):
            data_list.append(
                [description, m, self.bin_mux[self.mux_value],
                 self.voltage()])
            if n == 1:
                return data_list[0]
            if delay is not None:
                time.sleep(delay)
        return data_list

    def publish(self, description='NA', n=1, delay=None):
        """Get ADC data in JSON.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        str, formatted in JSON with keys:
            description : str
            n : sample number in this burst
            mux : XXX, multiplexer pin pair the voltage reading was taken with
            v : float, voltage measurement
        """
        data_list = []
        for m in range(n):
            data_list.append(
                self.json_writer.publish([
                    description, m, self.bin_mux[self.mux_value],
                    self.voltage()
                ]))
            if n == 1:
                return data_list[0]
            if delay is not None:
                time.sleep(delay)
        return data_list

    def write(self, description='NA', n=1, delay=None):
        """Get ADC output and save to file, formatted as either .csv or .json.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        None, writes to disk the following data:
            description : str
            n : sample number in this burst
            mux : XXX, multiplexer pin pair the voltage reading was taken with
            v : float, voltage measurement
        """
        wr = {
            "csv": self.csv_writer,
            "json": self.json_writer
        }[self.writer_output]
        for m in range(n):
            wr.write(
                [description, m, self.bin_mux[self.mux_value],
                 self.voltage()])
            if delay is not None:
                time.sleep(delay)
Example #14
0
class mpu6050:

    # Global Variables
    GRAVITIY_MS2 = 9.80665

    # State Variables
    accel_range = None
    gyro_range = None

    # Scale Modifiers
    ACCEL_SCALE_MODIFIER_2G = 16384.0
    ACCEL_SCALE_MODIFIER_4G = 8192.0
    ACCEL_SCALE_MODIFIER_8G = 4096.0
    ACCEL_SCALE_MODIFIER_16G = 2048.0

    GYRO_SCALE_MODIFIER_250DEG = 131.0
    GYRO_SCALE_MODIFIER_500DEG = 65.5
    GYRO_SCALE_MODIFIER_1000DEG = 32.8
    GYRO_SCALE_MODIFIER_2000DEG = 16.4

    # Pre-defined ranges
    ACCEL_RANGE_2G = 0x00
    ACCEL_RANGE_4G = 0x08
    ACCEL_RANGE_8G = 0x10
    ACCEL_RANGE_16G = 0x18

    GYRO_RANGE_250DEG = 0x00
    GYRO_RANGE_500DEG = 0x08
    GYRO_RANGE_1000DEG = 0x10
    GYRO_RANGE_2000DEG = 0x18

    # MPU-6050 Registers
    PWR_MGMT_1 = 0x6B
    PWR_MGMT_2 = 0x6C

    ACCEL_XOUT0 = 0x3B
    ACCEL_YOUT0 = 0x3D
    ACCEL_ZOUT0 = 0x3F

    TEMP_OUT0 = 0x41

    GYRO_XOUT0 = 0x43
    GYRO_YOUT0 = 0x45
    GYRO_ZOUT0 = 0x47

    ACCEL_CONFIG = 0x1C
    GYRO_CONFIG = 0x1B

    def __init__(self, bus_n, bus_addr=0x68, output='csv', name='MPU6050'):

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)

        # Wake up the MPU-6050 since it starts in sleep mode
        # by toggling bit6 from 1 to 0, see pg 40 of RM-MPU-6000A-00 v4.2
        self.bus.write_register_8bit(self.PWR_MGMT_1, 0x00)

        # information about this device
        self.metadata = Meta(name=name)
        self.metadata.description = 'TDK InvenSense Gyro & Accelerometer'
        self.metadata.urls = 'https://www.invensense.com/products/motion-tracking/6-axis/mpu-6050/'
        self.metadata.manufacturer = 'Adafruit Industries & TDK'

        # note: accuracy in datasheet is relative to scale factor - LSB/(deg/s) +/-3%
        # is there a better way to describe this? +/-3% below implies relative to deg/s output...
        self.metadata.header = [
            'description', 'sample_n', 'ax', 'ay', 'az', 'gx', 'gy', 'gz'
        ]
        self.metadata.dtype = [
            'str', 'int', 'float', 'float', 'float', 'float', 'float', 'float'
        ]
        self.metadata.units = [
            None, 'count', 'g', 'g', 'g', 'deg/s', 'deg/s', 'deg/s'
        ]
        self.metadata.accuracy = [
            None, 1, '+/-3%', '+/-3%', '+/-3%', '+/-3%', '+/-3%', '+/-3%'
        ]
        self.metadata.accuracy_precision_note = 'See datasheet for scale factor dependent accuracy & LSB precision'
        self.metadata.precision = None

        # specific specifications
        self.metadata.gyro_accuracy = '+/-3%, +/-2% cross axis'
        self.metadata.gyro_precision = '16bit'
        self.metadata.gyro_noise = '0.05 deg/s-rms'
        self.metadata.accel_accuracy = '+/-0.5%, +/-2 cross axis'
        self.metadata.accel_precision = '16bit'
        self.metadata.accel_noise = 'PSD 400 ug / Hz**1/2'

        self.metadata.bus_n = bus_n
        self.metadata.bus_addr = hex(bus_addr)

        # data recording classes
        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata,
                                    time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata,
                                      time_format='std_time_ms')

    # I2C communication methods

    def read_i2c_word(self, register):
        """Read two i2c registers and combine them.

        register -- the first register to read from.
        Returns the combined read results.
        """

        value = self.bus.read_register_16bit(register)

        if value >= 0x8000:
            return -((65535 - value) + 1)
        else:
            return value

    # MPU-6050 Methods

    def get_temp(self):
        """Reads the temperature from the onboard temperature sensor of the MPU-6050.

        Returns the temperature in degrees Celcius.
        """
        raw_temp = self.read_i2c_word(self.TEMP_OUT0)

        # Get the actual temperature using the formule given in the
        # MPU-6050 Register Map and Descriptions revision 4.2, page 30
        actual_temp = (raw_temp / 340.0) + 36.53

        return actual_temp

    def set_accel_range(self, accel_range):
        """Sets the range of the accelerometer to range.

        accel_range -- the range to set the accelerometer to. Using a
        pre-defined range is advised.
        """

        self.accel_range = accel_range

        # First change it to 0x00 to make sure we write the correct value later
        self.bus.write_register_16bit(self.ACCEL_CONFIG, 0x00)

        # Write the new range to the ACCEL_CONFIG register
        self.bus.write_register_16bit(self.ACCEL_CONFIG, accel_range)

    def read_accel_range(self, raw=False):
        """Reads the range the accelerometer is set to.

        If raw is True, it will return the raw value from the ACCEL_CONFIG
        register
        If raw is False, it will return an integer: -1, 2, 4, 8 or 16. When it
        returns -1 something went wrong.
        """
        raw_data = self.bus.read_register_16bit(self.ACCEL_CONFIG)

        if raw is True:
            return raw_data
        elif raw is False:
            if raw_data == self.ACCEL_RANGE_2G:
                return 2
            elif raw_data == self.ACCEL_RANGE_4G:
                return 4
            elif raw_data == self.ACCEL_RANGE_8G:
                return 8
            elif raw_data == self.ACCEL_RANGE_16G:
                return 16
            else:
                return -1

    def get_accel(self, g=False):
        """Gets and returns the X, Y and Z values from the accelerometer.

        If g is True, it will return the data in g
        If g is False, it will return the data in m/s^2
        Returns a dictionary with the measurement results.
        """

        x = self.bus.read_register_16bit(self.ACCEL_XOUT0)
        y = self.bus.read_register_16bit(self.ACCEL_YOUT0)
        z = self.bus.read_register_16bit(self.ACCEL_ZOUT0)

        accel_scale_modifier = None
        accel_range = self.read_accel_range(True)

        if accel_range == self.ACCEL_RANGE_2G:
            accel_scale_modifier = self.ACCEL_SCALE_MODIFIER_2G
        elif accel_range == self.ACCEL_RANGE_4G:
            accel_scale_modifier = self.ACCEL_SCALE_MODIFIER_4G
        elif accel_range == self.ACCEL_RANGE_8G:
            accel_scale_modifier = self.ACCEL_SCALE_MODIFIER_8G
        elif accel_range == self.ACCEL_RANGE_16G:
            accel_scale_modifier = self.ACCEL_SCALE_MODIFIER_16G
        else:
            print(
                "Unkown range - accel_scale_modifier set to self.ACCEL_SCALE_MODIFIER_2G"
            )
            accel_scale_modifier = self.ACCEL_SCALE_MODIFIER_2G

        x = x / accel_scale_modifier
        y = y / accel_scale_modifier
        z = z / accel_scale_modifier

        if g is True:
            return {'x': x, 'y': y, 'z': z}
        elif g is False:
            x = x * self.GRAVITIY_MS2
            y = y * self.GRAVITIY_MS2
            z = z * self.GRAVITIY_MS2
            return x, y, z

    def set_gyro_range(self, gyro_range):
        """Sets the range of the gyroscope to range.

        gyro_range -- the range to set the gyroscope to. Using a pre-defined
        range is advised.
        """

        self.gyro_range = gyro_range

        # First change it to 0x00 to make sure we write the correct value later
        self.bus.write_register_16bit(self.GYRO_CONFIG, 0x00)

        # Write the new range to the ACCEL_CONFIG register
        self.bus.write_register_16bit(self.GYRO_CONFIG, gyro_range)

    def read_gyro_range(self, raw=False):
        """Reads the range the gyroscope is set to.

        If raw is True, it will return the raw value from the GYRO_CONFIG
        register.
        If raw is False, it will return 250, 500, 1000, 2000 or -1. If the
        returned value is equal to -1 something went wrong.
        """

        raw_data = self.bus.read_register_16bit(self.GYRO_CONFIG)

        if raw is True:
            return raw_data
        elif raw is False:
            if raw_data == self.GYRO_RANGE_250DEG:
                return 250
            elif raw_data == self.GYRO_RANGE_500DEG:
                return 500
            elif raw_data == self.GYRO_RANGE_1000DEG:
                return 1000
            elif raw_data == self.GYRO_RANGE_2000DEG:
                return 2000
            else:
                return -1

    def get_gyro(self):
        """Gets and returns the X, Y and Z values from the gyroscope.

        Returns the read values in a dictionary.
        """
        x = self.read_i2c_word(self.GYRO_XOUT0)
        y = self.read_i2c_word(self.GYRO_YOUT0)
        z = self.read_i2c_word(self.GYRO_ZOUT0)

        gyro_scale_modifier = None
        gyro_range = self.read_gyro_range(True)

        if gyro_range == self.GYRO_RANGE_250DEG:
            gyro_scale_modifier = self.GYRO_SCALE_MODIFIER_250DEG
        elif gyro_range == self.GYRO_RANGE_500DEG:
            gyro_scale_modifier = self.GYRO_SCALE_MODIFIER_500DEG
        elif gyro_range == self.GYRO_RANGE_1000DEG:
            gyro_scale_modifier = self.GYRO_SCALE_MODIFIER_1000DEG
        elif gyro_range == self.GYRO_RANGE_2000DEG:
            gyro_scale_modifier = self.GYRO_SCALE_MODIFIER_2000DEG
        else:
            print(
                "Unkown range - gyro_scale_modifier set to self.GYRO_SCALE_MODIFIER_250DEG"
            )
            gyro_scale_modifier = self.GYRO_SCALE_MODIFIER_250DEG

        x = x / gyro_scale_modifier
        y = y / gyro_scale_modifier
        z = z / gyro_scale_modifier

        return x, y, z

    def get_all(self):
        """Reads and returns all the available data."""
        temp = self.get_temp()
        accel = self.get_accel()
        gyro = self.get_gyro()

        return [temp] + list(accel) + list(gyro)

    def get(self, description='NA', n=1, delay=None):
        """Get formatted output.

        Parameters
        ----------
        description : char, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        data : list, data containing:
            description: str, description of sample under test
            temperature : float, temperature in degrees Celcius
            delay : float, seconds to delay between samples if n > 1
        """
        data_list = []
        for m in range(1, n + 1):
            data_list.append([description, m] + list(self.get_accel()) +
                             list(self.get_gyro()))
            if n == 1:
                return data_list[0]
            if delay is not None:
                time.sleep(delay)
        return data_list

    def publish(self, description='NA', n=1, delay=None):
        """Output relay status data in JSON.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        str, formatted in JSON with keys:
            description: str, description of sample under test
            temperature : float, temperature in degrees Celcius
        """
        data_list = []
        for m in range(n):
            data_list.append(
                self.json_writer.publish([description, m] +
                                         list(self.get_accel()) +
                                         list(self.get_gyro())))
            if n == 1:
                return data_list[0]
            if delay is not None:
                time.sleep(delay)
        return data_list

    def write(self, description='NA', n=1, delay=None):
        """Format output and save to file, formatted as either
        .csv or .json.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1
        
        Returns
        -------
        None, writes to disk the following data:
            description : str, description of sample
            sample_n : int, sample number in this burst
            temperature : float, temperature in degrees Celcius
        """
        wr = {
            "csv": self.csv_writer,
            "json": self.json_writer
        }[self.writer_output]
        for m in range(n):
            wr.write([description, m] + list(self.get_accel()) +
                     list(self.get_gyro()))
            if delay is not None:
                time.sleep(delay)
Example #15
0
class DS3231:
    def __init__(self, bus_n, bus_addr=0x68, output='csv', name='DS3231'):
        """Initialize worker device on i2c bus.

        Parameters
        ----------
        bus_n : int, i2c bus number on Controller
        bus_addr : int, i2c bus number of this Worker device
        output : str, output data format, either 'csv' (default) or 'json'
        """

        # i2c bus
        self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr)

        # information about this device
        self.metadata = Meta(name=name)
        self.metadata.description = 'Adafruit DS3221 Precision RTC'
        self.metadata.urls = 'https://datasheets.maximintegrated.com/en/ds/DS3231.pdf'
        self.metadata.manufacturer = 'Adafruit Industries'

        self.metadata.header = [
            "description", "sample_n", "rtc_time", "temp_C"
        ]
        self.metadata.dtype = ['str', 'int', 'str', 'float']
        self.metadata.units = [None, 'count', 'datetime', 'degrees Celcius']
        self.metadata.accuracy = [None, 1, '+/- 3.5 ppm', '+/- 3.0']
        self.metadata.precision = [None, 1, '1 second', 0.25]

        self.metadata.bus_n = bus_n
        self.metadata.bus_addr = hex(bus_addr)

        # python strftime specification for RTC output precision
        self.metadata.rtc_time_format = '%Y-%m-%d %H:%M:%S'

        # data recording method
        # note: using millisecond accuracy on driver timestamp, even though
        # RTC is only 1 second resolution
        self.writer_output = output
        self.csv_writer = CSVWriter(metadata=self.metadata,
                                    time_format='std_time_ms')
        self.json_writer = JSONWriter(metadata=self.metadata,
                                      time_format='std_time_ms')

    def set_time(self, YY, MM, DD, hh, mm, ss, micro, tz):
        """Set time of RTC

        Parameters
        ----------
        YY : int, year range 1900 to 2100 (approx)
        MM : int, month, range 1 to 12
        DD : int, day, range 1 to 31
        hh : int, hour, range 0 to 23 (24hr clock) or 1-12 (12 hr clock)
        mm : int, minute, range 1-59
        ss : int, second, range 1-59
        micro : int, microseconds, range 0-999 (not implemented)
        tz : str, time zone (not implemented)
        """

        self.bus.write_register_8bit(reg_addr=0x00, data=dec2bcd(ss))
        self.bus.write_register_8bit(reg_addr=0x01, data=dec2bcd(mm))
        self.bus.write_register_8bit(reg_addr=0x02, data=dec2bcd(hh))
        self.bus.write_register_8bit(reg_addr=0x04, data=dec2bcd(DD))

        if YY >= 2000:
            MM = dec2bcd(MM) | 0b10000000
            YY = dec2bcd(YY - 2000)
        else:
            MM = dec2bcd(MM)
            YY = dec2bcd(YY - 1900)

        self.bus.write_register_8bit(reg_addr=0x05, data=MM)
        self.bus.write_register_8bit(reg_addr=0x06, data=YY)

    def get_time(self):
        """Get time from RTC

        Returns
        ----------
        YY : int, year range 1900 to 2100 (approx)
        MM : int, month, range 1 to 12
        DD : int, day, range 1 to 31
        hh : int, hour, range 0 to 23 (24hr clock) or 1-12 (12 hr clock)
        mm : int, minute, range 1-59
        ss : int, second, range 1-59
        """

        data = self.bus.read_register_nbit(reg_addr=0x00, n=7)

        ss = bcd2dec(data[0] & 0b01111111)
        mm = bcd2dec(data[1] & 0b01111111)
        if data[2] & 0b01000000 > 0:
            hh = bcd2dec(data[2] & 0b00011111)
            if data[2] & 0b00100000 > 0:
                hh += 12
        else:
            hh = bcd2dec(data[2] & 0b00111111)
        DD = bcd2dec(data[4] & 0b00111111)
        MM = bcd2dec(data[5] & 0b00011111)
        YY = bcd2dec(data[6])
        if data[5] & 0b10000000 > 0:
            YY = YY + 2000
        else:
            YY = YY + 1900
        return YY, MM, DD, hh, mm, ss

    def get_temp(self):
        """Get temperature of RTC

        Returns
        -------
        float, temperature in degrees Celsius
        """
        data = self.bus.read_register_nbit(reg_addr=0x11, n=2)
        return float(data[0]) + (data[1] >> 6) * 0.25

    def publish(self, description='NA', n=1, delay=None):
        """Get RTC time and temperature in JSON, plus metadata at intervals
        set by self.metadata_interval

        Parameters
        ----------
        description : char, description of data sample collected, default='NA'
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        str, formatted in JSON with keys:
            description : str
            n : sample number in this burst
            std_time : str, time formatted in YY-MM-DD hh:mm:ss
            temp_C : float, temperature of RTC in degrees C
        """
        data_list = []
        for m in range(n):
            std_time = '{:02d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}'.format(
                *self.get_time())
            temp_C = self.get_temp()
            data_list.append(
                self.json_writer.publish([description, 0, std_time, temp_C]))
            if n == 1:
                return data_list[0]
            if delay is not None:
                time.sleep(delay)
        return data_list

    def write(self, description='NA', n=1, delay=None):
        """Get RTC time and temperature and save to file,
        formatted as either .csv or .json.

        Parameters
        ----------
        description : str, description of data sample collected
        n : int, number of samples to record in this burst
        delay : float, seconds to delay between samples if n > 1

        Returns
        -------
        None, writes to disk the following data:
            description : str
            n : sample number in this burst
            std_time : str, time formatted in YY-MM-DD hh:mm:ss
            temp_C : float, temperature of RTC in degrees C
        """
        wr = {
            "csv": self.csv_writer,
            "json": self.json_writer
        }[self.writer_output]
        for m in range(n):
            std_time = '{:02d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}'.format(
                *self.get_time())
            temp_C = self.get_temp()
            wr.write([description, m, std_time, temp_C])
            if delay is not None:
                time.sleep(delay)