def __init__(self, bus_n, bus_addr=0x18, output='csv'): # 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.device = DeviceData("Qwiic Relay") self.device.description = ("Sparkfun Single Pole Double Throw Relay") self.device.urls = "https://learn.sparkfun.com/tutorials/qwiic-single-relay-hookup-guide/all" self.device.active = None self.device.error = None self.device.bus = repr(self.bus) self.device.manufacturer = "Sparkfun" self.device.version_hw = "1" self.device.version_sw = "1" self.application = "test" del self.device.accuracy del self.device.precision del self.device.dtype # data recording method self.writer_output = output self.csv_writer = CSVWriter("Qwiic Relay", time_format='std_time_ms') self.csv_writer.device = self.device.__dict__ self.csv_writer.header = ["sample_id", "state"] self.json_writer = JSONWriter("Qwiic Relay", time_format='std_time_ms') self.json_writer.device = self.device.__dict__ self.json_writer.header = ["sample_id", "state"]
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 __init__(self, bus_n, bus_addr=0x0F, output='csv'): # 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.device = DeviceData('Grove Motor Driver') self.device.description = ('I2C DC and stepper motor controller') self.device.urls = 'http://wiki.seeedstudio.com/Grove-I2C_Motor_Driver_V1.3/' self.device.active = None self.device.error = None self.device.bus = repr(self.bus) self.device.manufacturer = 'Seeed Studio' self.device.version_hw = '1.3' self.device.version_sw = '1.0' self.device.accuracy = None self.device.precision = '1 microstep' self.device.calibration_date = None # data recording method self.writer_output = output self.csv_writer = CSVWriter(self.device.name, time_format='std_time_ms') self.csv_writer.device = self.device.values() self.csv_writer.header = ['description', 'freq', 'm1_speed', 'm2_speed', 'm_direction', 'mode', 'phase', 'steps'] self.json_writer = JSONWriter(self.device.name, time_format='std_time_ms') self.json_writer.device = self.device.values() self.json_writer.header = self.csv_writer.header
def __init__(self, output='json'): # TODO: make safe for MicroPython, leave here for now in Conda from collections import deque from math import sin, pi from meerkat.data import CSVWriter, JSONWriter # data bus placeholder self.bus = None self.bus_addr = None # what kind of data output to file self.output = output # 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 # information about this device self.device = DeviceData('Software Test') self.device.description = 'Dummy data for software testing' self.device.urls = None self.device.manufacturer = None self.device.version_hw = None self.device.version_sw = None self.device.accuracy = None self.device.precision = None self.device.bus = None self.device.state = 'Test Not Running' self.device.active = False self.device.error = None self.device.dtype = None self.device.calibration_date = None # data writer if self.output == 'csv': self.writer = CSVWriter('Software Test') #self.writer.device = self.device.values() elif self.output == 'json': self.writer = JSONWriter('Software Test') self.writer.header = ['index', 'degrees', 'amplitude'] self.writer.device = self.device.values() # example data of one 360 degree, -1 to 1 sine wave self._deg = [n for n in range(360)] self._amp = [sin(d * (pi / 180.0)) for d in self._deg] self._test_data = list(zip(self._deg, self._amp))
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 __init__(self, bus_n, bus_addr=0x18, output='csv', name='mcp9808'): """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.metadata = Meta(name=name) self.metadata.description = 'Microchip Tech digital temperature sensor' self.metadata.urls = 'https://www.microchip.com/datasheet/MCP9808' self.metadata.manufacturer = 'Adafruit Industries & Microchip Tech' self.metadata.header = ['description', 'sample_n', 'temp_C'] self.metadata.dtype = ['str', 'int', 'float'] self.metadata.units = [None, 'count', 'degrees Celcius'] self.metadata.accuracy = [None, 1, '+/- 0.25 typical'] self.metadata.precision = [None, 1, '0.0625 max'] 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 __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 __init__(self, bus_n, bus_addr=0x68, output='csv'): # 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.device = DeviceData('MPU-6050') self.device.description = ('TDK InvenSense Gyro & Accelerometer') self.device.urls = 'https://www.invensense.com/products/motion-tracking/6-axis/mpu-6050/' self.device.active = None self.device.error = None self.device.bus = repr(self.bus) self.device.manufacturer = 'TDK' self.device.version_hw = '0.1' self.device.version_sw = '0.1' self.device.gyro_accuracy = '+/-3%, +/-2% cross axis' self.device.gyro_precision = '16bit' self.device.gyro_noise = '0.05 deg/s-rms' self.device.accel_accuracy = '+/-0.5%, +/-2 cross axis' self.device.accel_precision = '16bit' self.device.accel_noise = 'PSD 400 ug / Hz**1/2' self.device.calibration_date = None ''' # data recording method if output == 'csv': self.writer = CSVWriter('MPU-6050', time_format='std_time_ms') self.writer.header = ['description', 'sample_n', 'arange', 'grange', 'ax', 'ay', 'az', 'gx', 'gy', 'gz', 'temp_C'] elif output == 'json': self.writer = JSONWriter('MPU-6050', time_format='std_time_ms') else: pass # holder for another writer or change in default self.writer.device = self.device.values() ''' # data recording information self.sample_id = None # data recording method self.writer_output = output self.csv_writer = CSVWriter("MPU-6050", time_format='std_time_ms') self.csv_writer.device = self.device.__dict__ self.csv_writer.header = [ 'description', 'sample_n', 'ax', 'ay', 'az', 'gx', 'gy', 'gz' ] self.json_writer = JSONWriter("MCP9808", time_format='std_time_ms') self.json_writer.device = self.device.__dict__ self.json_writer.header = self.csv_writer.header
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')
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')
def __init__(self, bus_n, bus_addr=0x00, 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 = base.I2C(bus_n=bus_n, bus_addr=bus_addr) # print debug statements self.verbose = False # information about this device self.device = base.DeviceData('ExampleDevice') self.device.description = ('Just an example, replace') self.device.urls = 'www.example.com' self.device.active = None self.device.error = None self.device.bus = repr(self.bus) self.device.manufacturer = 'None' self.device.version_hw = '1.0' self.device.version_sw = '1.0' self.device.accuracy = None self.device.precision = 'replace' self.device.calibration_date = None # add device specific attributes self.device.other_attributes = None # data recording method if output == 'csv': self.writer = CSVWriter('ExampleDevice', time_format='std_time_ms') elif output == 'json': self.writer = JSONWriter('ExampleDevice', time_format='std_time_ms') else: pass # holder for another writer or change in default # set writer header for device's output format, ADS1115 shown example self.writer.header = ['description', 'sample_n', 'voltage', 'current'] self.writer.device = self.device.values() # data recording information self.sample_id = None # intialized configuration values self.get_config()
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 __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 __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 __init__(self, bus_n, bus_addr, output='csv', name='atlas_base'): """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 # data recording information self.sample_id = None # information about this device self.metadata = Meta(name=name) self.metadata.description = 'Base class for Atlas Scientific Devices' self.metadata.urls = 'www.atlas-scientific.com' self.metadata.manufacturer = 'Atlas Scientific' self.metadata.header = None self.metadata.dtype = None 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 __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')
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)
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']
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))
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))
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))
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)
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)
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)
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()
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])
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 __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')
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)
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)