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))
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 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 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 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)
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)
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])
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)
class MCP9808(object): def __init__(self, bus_n, bus_addr=0x18, output='csv'): """Initialize worker device on i2c bus. Parameters ---------- bus_n : int, i2c bus number on Controller bus_addr : int, i2c bus number of this Worker device """ # i2c bus self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr) # register values and defaults # TODO: do the constant values need to be specified? self.reg_map = { 'config': REG_CONFIG, 'upper_temp': REG_UPPER_TEMP, 'lower_temp': REG_LOWER_TEMP, 'crit_temp': REG_CRIT_TEMP, 'ambient': REG_AMBIENT_TEMP, 'manufacturer': REG_MANUF_ID, 'device_id': REG_DEVICE_ID } self.alert_critial = None self.alert_upper = None self.alert_lower = None self.manufacturer_id = None self.device_id = None self.revision = None # information about this device self.device = DeviceData('MCP9808') self.device.description = ( '+/-0.5 degrees Celcius ' + 'maximum accuracy digital temperature sensor') self.device.urls = 'https://www.microchip.com/datasheet/MCP9808' self.device.active = None self.device.error = None self.device.bus = repr(self.bus) self.device.manufacturer = 'Microchip' self.device.version_hw = '0.1' self.device.version_sw = '0.1' self.device.accuracy = '+/-0.25 (typical) C' self.device.precision = '0.0625 C maximum' self.device.units = 'Degrees Celcius' self.device.calibration_date = None # data recording information self.sample_id = None # data recording method self.writer_output = output self.csv_writer = CSVWriter("MCP9808", time_format='std_time_ms') self.csv_writer.device = self.device.__dict__ self.csv_writer.header = ['description', 'sample_n', 'temperature'] self.json_writer = JSONWriter("MCP9808", time_format='std_time_ms') self.json_writer.device = self.device.__dict__ self.json_writer.header = ['description', 'sample_n', 'temperature'] def set_pointer(self, reg_name): """Set the pointer register address Allowed address names: 'config' 'upper_temp' 'lower_temp' 'crit_temp' 'ambient' 'manufacturer' 'device_id' Parameters ---------- reg_name : str, register address """ reg_addr = self.reg_map[reg_name] self.bus.write_byte(reg_addr) def read_register_16bit(self, reg_name): """Get the values from one registry Allowed register names: 'config' 'upper_temp' 'lower_temp' 'crit_temp' 'ambient' 'manufacturer' 'device_id' Parameters ---------- reg_name : str, name of registry to read Returns ------- upper byte lower byte """ reg_addr = self.reg_map[reg_name] ulb = self.bus.read_register_16bit(reg_addr) ub = ulb >> 8 lb = ulb & 0xff return ub, lb def get_status(self): self.get_config() self.get_upper_temp() self.get_lower_temp() self.get_critical_temp() self.get_manufacturer() self.get_device_id() def print_status(self): self.get_status() print('Configuration Register: {}'.format(None)) print('Upper Temperature: {}'.format(None)) print('Lower Temperature: {}'.format(None)) print('Critical Temperature: {}'.format(None)) print('Manufacturer: {}'.format(self.manufacturer_id)) print('Device ID: {}'.format(self.device_id)) print('Device Revision: {}'.format(self.revision)) def get_config(self): #TODO: parse bytes and add docstrings ub, lb = self.read_register_16bit('config') def get_upper_temp(self): ub, lb = self.read_register_16bit('upper_temp') def get_lower_temp(self): ub, lb = self.read_register_16bit('lower_temp') def get_critical_temp(self): ub, lb = self.read_register_16bit('crit_temp') def get_manufacturer(self): ub, lb = self.read_register_16bit('manufacturer') self.manufacturer_id = (ub << 8) | lb def get_device_id(self): (self.device_id, self.revision) = self.read_register_16bit('device_id') def get_temp(self): """Get temperature in degrees Celcius with 13 bit accuracy Returns ------- float, temperature in degrees Celcius """ ub, lb = self.read_register_16bit('ambient') self.alert_critial = (ub & 0x80) == 0x80 self.alert_upper = (ub & 0x40) == 0x40 self.alert_lower = (ub & 0x20) == 0x20 ub = ub & 0x1F if (ub & 0x10) == 0x10: ub = ub & 0x10 return 256 - (ub * 2**4) + (lb * 2**-4) else: return (ub * 2**4) + (lb * 2**-4) def get(self, description='NA', n=1, delay=None): """Get formatted output. Parameters ---------- description : char, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- data : list, data containing: description: str, description of sample under test temperature : float, temperature in degrees Celcius delay : float, seconds to delay between samples if n > 1 """ data_list = [] for m in range(1, n + 1): data_list.append([description, m, self.get_temp()]) if n == 1: return data_list[0] if delay is not None: time.sleep(delay) return data_list def publish(self, description='NA', n=1, delay=None): """Output relay status data in JSON. Parameters ---------- description : str, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- str, formatted in JSON with keys: description: str, description of sample under test temperature : float, temperature in degrees Celcius """ data_list = [] for m in range(n): data_list.append( self.json_writer.publish([description, m, self.get_temp()])) if n == 1: return data_list[0] if delay is not None: time.sleep(delay) return data_list def write(self, description='NA', n=1, delay=None): """Format output and save to file, formatted as either .csv or .json. Parameters ---------- description : str, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- None, writes to disk the following data: description : str, description of sample sample_n : int, sample number in this burst temperature : float, temperature in degrees Celcius """ wr = { "csv": self.csv_writer, "json": self.json_writer }[self.writer_output] for m in range(n): wr.write([description, m, self.get_temp()]) if delay is not None: time.sleep(delay)
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 Atlas: """Base class for Atlas Scientific sensors""" def __init__(self, bus_n, bus_addr, output='csv'): """Initialize worker device on i2c bus. Parameters ---------- bus_n : int, i2c bus number on Controller bus_addr : int, i2c bus number of this Worker device """ # i2c bus self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr) # time to wait for conversions to finish self.short_delay = 0.3 # seconds for regular commands self.long_delay = 1.5 # seconds for readings self.cal_delay = 3.0 # seconds for calibrations # information about this device self.device = DeviceData('Atlas_Base') self.device.description = ('') self.device.urls = 'www.atlas-scientific.com' self.device.active = None self.device.error = None self.device.bus = repr(self.bus) self.device.manufacturer = 'Atlas Scientific' self.device.version_hw = '1.0' self.device.version_sw = '1.0' self.device.accuracy = None self.device.precision = 'Varies' self.device.calibration_date = None """ # data recording method if output == 'csv': self.writer = CSVWriter('Atlas_Base', time_format='std_time_ms') elif output == 'json': self.writer = JSONWriter('Atlas_Base', time_format='std_time_ms') else: pass # holder for another writer or change in default self.writer.header = ['description', 'sample_n', 'not_set'] self.writer.device = self.device.values() """ # data recording information self.sample_id = None # data recording method self.writer_output = output self.csv_writer = CSVWriter("Atlas_Base", time_format='std_time_ms') self.csv_writer.device = self.device.__dict__ self.csv_writer.header = ['description', 'sample_n', 'not_set'] self.json_writer = JSONWriter("Atlas_Base", time_format='std_time_ms') self.json_writer.device = self.device.__dict__ self.json_writer.header = self.csv_writer.header def query(self, command, n=31, delay=None, verbose=False): """Write a command to the i2c device and read the reply, delay between reply based on command value. Byte codes: First byte repsonse codes 0x1 = successful request 0x2 = syntax error 0x254 = still processing, not ready 0x255 = no data to send Filler byte 0x0 = filler (usually at the end of a reply) Parameters ---------- command : str, command to execute on the device n : int, number of bytes to read delay : float, number of milliseconds to delay before reading response verbose : bool, print debug statements Returns ------- str : response, may require further parsing """ if verbose: print("Input Type: ", type(command)) if (type(command) == int) or (type(command) == bytes): byte_command = command else: byte_command = [ord(x) for x in command] if verbose: print("Sent:", command) self.bus.write_n_bytes(*[byte_command]) if delay is not None: time.sleep(delay/1000) if n != 0: reply = self.bus.read_n_bytes(n=n) reply_mapper = {0: 'Filler', 1: 'Success', 254: 'Still processing', 255: 'No data'} if verbose: # print the response code that is in position 0 of reply reply_0 = reply[0] print("Reply:", reply_mapper[reply_0]) # filter out response codes and filler bytes reply_bytes = bytearray([]) for reply_n in reply: if reply_n not in [0x0, 0x1, 0x2, 0x254, 0x255]: reply_bytes.append(reply_n) reply_bytes = bytes(reply_bytes) reply_bytes = reply_bytes.decode('utf-8') if verbose: print("Formatted and trimmed reply:", reply_bytes) return reply_bytes def led_on(self): """Turn on status LED until another character is set""" self.query(b'L,1', n=0) def led_off(self): """Turn off status LED""" self.query(b'L,0', n=0) def find_start(self): """Blink white LED until another character is set""" self.query(b'Find', n=0) def find_stop(self): """Stop white LED from blinking""" self.query(0x01, n=0) def info(self): """Get device information Returns ------- device: str, device type firmware : str, firmware version """ _r = self.query(b'i', n=15, delay=350) _, device, firmware = _r.split(",") return device, firmware def status(self): """Get device status Returns ------- restart code : str, one character meaning P = power off S = software reset B = brown out W = watchdog U = unknown vcc : float, supply voltage of input to device """ _r = self.query(b'Status', n=15, delay=350) _, restart_code, vcc = _r.split(",") return restart_code, float(vcc) def sleep(self): """Put device to sleep. Any byte sent wakes.""" _r = self.query(b'Sleep', n=0) def wake(self): """Wake device from sleep state""" _r = self.query(0x01, n=0) def plock_status(self): """Get protocol lock status""" _r = self.query(b'Plock,?', n=9, delay=350, verbose=verbose) _, plock_state = _r.split(",") return int(plock_state) def plock_on(self): """Lock device into I2C mode""" _r = self.query(b'Plock,1', n=0) def plock_off(self): """Unlock device from I2C mode""" _r = self.query(b'Plock,0', n=0) def reset(self): """Completely reset device. Clears calibration, sets LED on and enables reponse codes""" _r = self.query(b'Factory', n=0) def change_i2c_bus_address(self, n): """Change the device I2C bus address - device will not be accessible until contacted at new I2C address. Response is device reboot. Parameters ---------- n : int, I2C address, can be any number 1-127 inclusive """ _r = self.query(bytes("I2C,{}".format(n), encoding='utf-8'), n=0) def get(self, description='no_description', n=1, delay=0): """Get formatted output, assumes subclass has method 'measure' Parameters ---------- description : char, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- data : list, data that will be saved to disk with self.write containing: description : str c : float, sensor measurement """ data_list = [] for m in range(n): measure = self.measure() if isinstance(measure, float): measure = [measure] data = [description, m] + measure data_list.append(data) if n == 1: return data_list[0] time.sleep(max(self.long_delay, delay)) return data_list def publish(self, description='NA', n=1, delay=0): """Output relay status data in JSON. Parameters ---------- description : str, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- str, formatted in JSON with keys: description: str, description of sample under test measurement : float, measurement made """ data_list = [] for m in range(n): measure = self.measure() if isinstance(measure, float): measure = [measure] data_list.append(self.json_writer.publish([description, m] + measure)) if n == 1: return data_list[0] time.sleep(max(self.long_delay, delay)) return data_list def write(self, description='NA', n=1, delay=0): """Format output and save to file, formatted as either .csv or .json. Parameters ---------- description : str, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- None, writes to disk the following data: description : str, description of sample sample_n : int, sample number in this burst voltage, float, Volts measured at the shunt resistor current : float, Amps of current accross the shunt resistor """ wr = {"csv": self.csv_writer, "json": self.json_writer}[self.writer_output] for m in range(n): measure = self.measure() if ((isinstance(measure, float)) or (isinstance(measure, int)) or (isinstance(measure, str))): measure = [measure] wr.write([description, m] + measure) time.sleep(max(self.long_delay, delay))
class PA1010D(Base): def __init__(self, bus_n, bus_addr=0x10, output='csv', name='pa1010d'): """PA1010D GPS module using MTK3333 chipset Supported NMEA sentences: 'GGA', 'GSA', 'GSV', 'RMC', 'VTG' Parameters ---------- bus_n : int, i2c bus number on Controller bus_addr : int, i2c bus number of this Worker device nmea_sentence : str, NMEA sentence type to save for CSV. JSON will save """ # i2c bus self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr) # maximum data in buffer size self._bytes_per_burst = 255 # standard metadata information about this device self.metadata = Meta(name=name) self.metadata.description = 'Adafruit PA1010D GPS/GNSS module' self.metadata.urls = 'https://www.cdtop-tech.com/products/pa1010d' self.metadata.manufacturer = 'CDTop Technology' self.metadata.state = None self.metadata.header = ['description', 'sample_n', 'nmea_sentence'] self.metadata.dtype = ['str', 'int', 'str'] self.metadata.units = None self.metadata.accuracy = None self.metadata.precision = '<3.0 meters' self.metadata.bus_n = bus_n self.metadata.bus_addr = hex(bus_addr) # custom metadata attributes self.metadata.supported_nmea_sentences = ['GGA', 'GSA', 'GSV', 'RMC', 'VTG'] # data recording method self.writer_output = output self.csv_writer = CSVWriter(metadata=self.metadata, time_format='std_time_ms') self.json_writer = JSONWriter(metadata=self.metadata, time_format='std_time_ms') def raw_get(self): """Get byte data from the GPS module, filtered of blanks and combining lines""" data = "" _d = self.bus.read_n_bytes(n=self._bytes_per_burst) _d = _d.decode("UTF-8") data = "".join([data, _d]) data = regex.split(data) data = [_d for _d in data if _d != ""] data = set(data) data_out = [] last_line = "" started = False for line in data: if (line[0] == "$") & ("*" in line): data_out.append(line) elif ("$" in line): last_line = line elif ("*" in line) & (last_line != ""): data_out.append(last_line + line) return data_out def get(self, nmea_sentences=None, timeout=15): """Get NMEA sentences Parameters ---------- nmea_sentence : list of str, NMEA sentence codes to return i.e. 'GSV' timeout : int, seconds to wait for data before returning Returns ------- list of str, NMEA sentences """ if nmea_sentences is None: nmea_sentences = self.metadata.supported_nmea_sentences check = {s: False for s in nmea_sentences} t0 = time.time() while sum([x == False for x in check.values()]) > 0: if time.time() - t0 > timeout: # timeout in seconds break for single_line in self.raw_get(): sentence_type = single_line[3:6] if (sentence_type in nmea_sentences) & ("*" in single_line): gps_data = single_line.split("*") gps_cs = gps_data[1] gps_data = gps_data[0][1:] driver_cs = calc_checksum(gps_data) if gps_cs == driver_cs: check[sentence_type] = single_line return [v for k, v in check.items()] def send_command(self, command, add_format=True): """Send a command string to the GPS. Note: If add_format=True, do not add leading '$' and trailing '*' plus checksum, these will be added automatically Parameters ---------- command : str, command to send add_format : bool, add '$' prefix and '*' plus calculated checksum """ if add_format: checksum = calc_checksum(command) command = "${}*{}\r\n".format(command, checksum) command = bytes(command) self.bus.write_n_bytes(command) def full_power_mode(self): """Enable full power continuous mode""" self.send_command(b"$PMTK225,0*2B", add_format=False) def standby_mode(self): """Enable standby power mode""" self.send_command(b"$PMTK161,0*28", add_format=False) def periodic_mode(self, mode_type, rt1, st1, rt2, st2): """Enable periodic power on/off mode Parameters ---------- mode_type : int, 1 or 2 where: 1 = periodic backup mode 2 = periodic standby mode See other mode method for descriptions rt1 : int, full power mode length in milliseconds st1 : int, mode_type length in milliseconds rt2 : int, full power mode length in milliseconds st2 : int, mode_type length in milliseconds """ assert mode_type in [1,2] command = "PMTK225{},{},{},{},{}".format(mode_type, rt1, st1, rt2, st2) command = bytes(command) self.send_command(command, add_format=True) def always_locate_mode(self): """Enable always locate mode""" self.send_command(b"$PMTK225,8*23", add_format=False) def backup_mode(self): """Enable backup mode Note: Before sending the command the WAKE-UP pin (pin 2) must be tied to ground. It is not possible to wake up the module from backup mode by software command.""" self.send_command(b"$PMTK225,4*2F", add_format=False) def publish(self, description='NA', n=1, nmea_sentences=None, delay=None): """Output relay status data in JSON. Parameters ---------- description : str, description of data sample collected n : int, number of samples to record in this burst nmea_sentence : list of str, NMEA sentence codes to return i.e. 'GSV' delay : float, seconds to delay between samples if n > 1 Returns ------- str, formatted in JSON with keys: description : str n : sample number in this burst nmea_sentence : str, NMEA sentence """ if nmea_sentences is None: nmea_sentences = self.metadata.supported_nmea_sentences data_list = [] for m in range(n): nmea_data = self.get(nmea_sentences=nmea_sentences) for p in range(len(nmea_data)): data = self.json_writer.publish([description, m, nmea_data[p]]) data_list.append(data) if delay is not None: time.sleep(delay) return data_list def write(self, description='NA', n=1, nmea_sentences=None, delay=None): """Format output and save to file, formatted as either .csv or .json. Parameters ---------- description : str, description of data sample collected n : int, number of samples to record in this burst nmea_sentence : list of str, NMEA sentence codes to return i.e. 'GSV' delay : float, seconds to delay between samples if n > 1 Returns ------- None, writes to disk the following data: description : str n : sample number in this burst nmea_sentence : str, NMEA sentence """ if nmea_sentences is None: nmea_sentences = self.metadata.supported_nmea_sentences wr = {"csv": self.csv_writer, "json": self.json_writer}[self.writer_output] for m in range(n): nmea_data = self.get(nmea_sentences=nmea_sentences) for p in range(len(nmea_data)): wr.write([description, m, '"' + nmea_data[p] + '"']) if delay is not None: time.sleep(delay)
class ADS1115(object): def __init__(self, bus_n, bus_addr=0x48, output='csv', name='ADS1115'): """Initialize worker device on i2c bus. Parameters ---------- bus_n : int, i2c bus number on Controller bus_addr : int, i2c bus number of this Worker device """ # i2c bus self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr) # time to wait for conversion to finish self.delay = 0.009 # units = seconds # register values and defaults self.conversion_value = 40000 # higher than any conversion result self.config_value = None # 0x8583 default self.lo_thres_value = None # 0x8000 default self.hi_thres_value = None # 0x7fff default self.reg_map = { 'conversion': 0b00, 'config': 0b01, 'lo_thresh': 0b10, 'hi_thresh': 0b11 } # config register attributes and chip defaults self.comp_que_value = 0b11 self.comp_lat_value = 0b0 self.comp_pol_value = 0b0 self.comp_mode_value = 0b0 self.dr_value = 0b100 self.mode_value = 0b1 self.pga_value = 0b010 self.mux_value = 0b000 self.os_value = 0b0 # voltage measurement self.pga_float = -999 self.volts = -999 # attribute converters self.str_mux = { '01': 0b000, '03': 0b001, '13': 0b010, '23': 0b011, '0G': 0b100, '1G': 0b101, '2G': 0b110, '3G': 0b111 } self.bin_mux = {v: k for k, v in self.str_mux.items()} self.str_pga = { '6.144': 0b000, '4.096': 0b001, '2.048': 0b010, '1.024': 0b011, '0.512': 0b100, '0.256': 0b101 } self.bin_pga = {v: float(k) for k, v in self.str_pga.items()} self.str_mode = {'continuous': 0b0, 'single': 0b1} self.bin_mode = {v: k for k, v in self.str_mode.items()} self.str_data_rate = { 8: 0b000, 16: 0b001, 32: 0b010, 64: 0b011, 128: 0b100, 250: 0b101, 475: 0b110, 860: 0b111 } self.bin_data_rate = {v: k for k, v in self.str_data_rate.items()} self.str_comp_mode = {'trad': 0b0, 'window': 0b1} self.bin_comp_mode = {v: k for k, v in self.str_comp_mode.items()} self.str_comp_pol = {'1': 0b00, '2': 0b01, '3': 0b10, 'off': 0b11} self.bin_comp_pol = {v: k for k, v in self.str_comp_pol.items()} self.str_comp_lat = {'off': 0b0, 'on': 0b1} self.bin_comp_lat = {v: k for k, v in self.str_comp_lat.items()} self.str_comp_que = {'1': 0b00, '2': 0b01, '3': 0b10, 'off': 0b11} self.bin_comp_que = {v: k for k, v in self.str_comp_que.items()} # information about this device self.metadata = Meta(name=name) self.metadata.description = ('Texas Instruments 16-bit 860SPS' + ' 4-Ch Delta-Sigma ADC with PGA') self.metadata.urls = 'www.ti.com/product/ADS1115' self.metadata.manufacturer = 'Texas Instruments' self.metadata.header = ['description', 'sample_n', 'mux', 'voltage'] self.metadata.dtype = ['str', 'int', 'str', 'float'] self.metadata.units = [None, 'count', 'str', 'volts'] self.metadata.accuracy = [None, 1, None, '+/- 3 LSB'] self.metadata.precision = [None, 1, None, '16 bit'] self.metadata.bus_n = bus_n self.metadata.bus_addr = hex(bus_addr) # current settings of this device self.metadata.pga_gain = self.pga_float # data recording information self.sample_id = None self.writer_output = output self.csv_writer = CSVWriter(metadata=self.metadata, time_format='std_time_ms') self.json_writer = JSONWriter(metadata=self.metadata, time_format='std_time_ms') # initialize class attributes from device registry self.get_config() def twos_comp_to_dec(self, value, bits): """Convert Two's Compliment format to decimal""" if (value & (1 << (bits - 1))) != 0: value = value - (1 << bits) return value def set_pointer(self, reg_name): """Set the pointer register address Allowed register names: 'conversion' 'config' 'lo_thres' 'hi_thresh' Parameters ---------- reg_name : str, register address """ reg_addr = self.reg_map[reg_name] self.bus.write_byte(reg_addr) def read_register_16bit(self, reg_name): """Get the values from one registry Allowed register names: 'conversion' 'config' 'lo_thres' 'hi_thresh' Parameters ---------- reg_name : str, name of registry to read Returns ------- 16 bit registry value """ reg_addr = self.reg_map[reg_name] return self.bus.read_register_16bit(reg_addr) def write_register_16bit(self, reg_name, data): """Write a 16 bits of data to register Allowed register names: 'conversion' 'config' 'lo_thres' 'hi_thresh' Parameters ---------- reg_name : str, name of registry to read data : int, 16 bit value to write to register """ reg_addr = self.reg_map[reg_name] self.bus.write_register_16bit(reg_addr, data) return True def set_config(self): self.write_register_16bit('config', self.config_value) return True def get_conversion(self): """Read the ADC conversion register at address 0x00 Default value on power up from chip is 0 """ self.conversion_value = self.read_register_16bit('conversion') return self.conversion_value def get_config(self): """Read the configuration register at address 0x01 Default value from chip is 0x8583 = 34179 = '0b1000010110000110' Specific states extracted with other methods """ self.config_value = self.read_register_16bit('config') self.update_attributes() def update_attributes(self): """Update all attributes TODO: add more detail """ # Comparator queue and disable self.comp_que_value = self.config_value & 0b11 # Latching comparator self.comp_lat_value = (self.config_value >> 2) & 0b1 # Comparator polarity self.comp_pol_value = (self.config_value >> 3) & 0b1 # Comparator mode self.comp_mode_value = (self.config_value >> 4) & 0b1 # Data rate self.dr_value = (self.config_value >> 5) & 0b111 # Operating mode self.mode_value = (self.config_value >> 8) & 0b1 # Programmable gain amplifier self.pga_value = (self.config_value >> 9) & 0b111 self.pga_float = self.bin_pga[self.pga_value] # Multiplexer self.mux_value = (self.config_value >> 12) & 0b111 # Operational status / Single-shot conversion start self.os_value = self.config_value >> 15 def get_lo(self): """Read the low threshold register at address 0x02 Default value from chip is 0x8000 """ self.lo_thres_value = self.read_register_16bit('lo_thresh') return self.lo_thres_value def get_hi(self): """Read the high threshold register at address 0x03 Default value from chip is 0x7FFF """ self.hi_thres_value = self.read_register_16bit('hi_thresh') return self.hi_thres_value def os(self): """Set the operational status As this has only one use in write mode, sets bit 15 to True. Chip will automatically clear it. Bit 15 = True is also the power on default - it should be sufficient to read the configuration register once and use that bit position for all read commands. """ self.config_value = (self.config_value & BIT_OS) | (0b1 << 15) self.set_config() self.os_value = self.config_value >> 15 def mux(self, x): """Set multiplexer pin pair, ADS1115 only. Parameters ---------- x : str, positive and negative pin combination. Based on: AIN pins '0', '1', '2', '3' and Ground pin 'G' i.e. for AIN_pos = AIN0 and AIN_neg = Ground, x = '0G' """ self.config_value = ((self.config_value & BIT_MUX) | (self.str_mux[x] << 12)) self.set_config() time.sleep(self.delay) self.mux_value = (self.config_value >> 12) & 0b111 def pga(self, x): """Set programmable gain amplifier range. Parameters ---------- x : str, +/- voltage range value. Supported values: '6.144', '4.096', '2.048', '1.024', '0.512', '0.256' """ self.config_value = ((self.config_value & BIT_PGA) | (self.str_pga[x] << 9)) self.set_config() time.sleep(self.delay) # needs at least 7 ms to complete self.pga_value = (self.config_value >> 9) & 0b111 self.pga_float = self.bin_pga[self.pga_value] def mode(self, x): """Set operating mode to either single or continuous. Parameters ---------- x: str, either 'single' or 'continuous' """ self.config_value = ((self.config_value & BIT_MODE) | (self.str_mode[x] << 8)) self.set_config() time.sleep(self.delay) self.mode_value = (self.config_value >> 8) & 0b1 def data_rate(self, x): """Set data rate of sampling Changes bits [7:5] Parameters ---------- x : int, samples per second. Allowed values: 8, 16, 32, 64, 128, 250, 475, 860 """ self.config_value = ((self.config_value & BIT_DR) | (self.str_data_rate[x] << 5)) self.set_config() self.dr_value = (self.config_value >> 5) & 0b111 def comp_mode(self, x): """Set comparator mode ADS1114 and ADS1115 only, changes bit 4 x : str, 'trad' or 'window' """ self.config_value = ((self.config_value & BIT_COMP_MODE) | (self.str_comp_mode[x] << 4)) self.set_config() self.comp_mode_value = (self.config_value >> 4) & 0b1 def comp_polarity(self, x): """Set polarity of ALERT/RDY pin when active. No function in ADS1113, changes bit 3 Parameters ---------- x : str, 'high' or 'low' """ self.config_value = ((self.config_value & BIT_COMP_POL) | (self.str_comp_pol[x] << 3)) self.set_config() self.comp_pol_value = (self.config_value >> 3) & 0b1 def comp_latching(self, x): """Set whether the ALERT/RDY pin latches or clears when conversions are within the margins of the upper and lower thresholds Only available in ADS1114 and ADS1115, default is 0 = non-latching Parameters ---------- x : str, 'on' or 'off' """ self.config_value = ((self.config_value & BIT_COMP_LAT) | (self.str_comp_lat[x] << 2)) self.set_config() self.comp_lat_value = (self.config_value >> 2) & 0b1 def comp_que(self, x): """Disable or set the number of conversions before a ALERT/RDY pin is set high Parameters ---------- x : str, number of conversions '1', '2', '4' or 'off' """ self.config_value = ((self.config_value & BIT_COMP_QUE) | (self.str_comp_lat[x] << 0)) self.set_config() self.comp_que_value = self.config_value & 0b11 def single_shot(self): """Write 0x1 to bit 15 of the configuration register to initialize a single shot conversion. The configuration register must be read at least once to get the current configuration, otherwise the chip default is used. Chip clears bit on completion of ADC conversion. """ self.os() time.sleep(self.delay) self.get_conversion() def voltage(self): """Calculate the voltage measured by the chip based on conversion register and configuration register values """ if self.mode_value == 1: self.single_shot() else: self.get_conversion() _x = self.twos_comp_to_dec(self.conversion_value, 16) self.volts = _x * (self.pga_float / 2**15) return self.volts def print_attributes(self): """Print to console current attributes""" print('ADS11x5 Configuration Attributes') print('--------------------------------') print('Config Register:', self.config_value, hex(self.config_value), bin(self.config_value)) print('PGA Range: +/-', self.pga_float, 'Volts') print('Mode:', self.bin_mode[self.mode_value]) print('Data Rate:', self.bin_data_rate[self.dr_value], 'SPS') print('Input Multiplexer:', self.bin_mux[self.mux_value]) print('Comparator:') print(' Queue:', self.bin_comp_que[self.comp_que_value]) print(' Latching:', self.bin_comp_lat[self.comp_lat_value]) print(' Polarity: Active', self.bin_comp_pol[self.comp_pol_value]) print(' Mode:', self.bin_comp_mode[self.comp_mode_value]) def get(self, description='no_description', n=1, delay=None): """Get ADC data. Parameters ---------- description : char, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- data : list, data that will be saved to disk with self.write containing: description : str n : sample number in this burst mux : XXX, multiplexer pin pair the voltage reading was taken with v : float, voltage measurement """ data_list = [] for m in range(1, n + 1): data_list.append( [description, m, self.bin_mux[self.mux_value], self.voltage()]) if n == 1: return data_list[0] if delay is not None: time.sleep(delay) return data_list def publish(self, description='NA', n=1, delay=None): """Get ADC data in JSON. Parameters ---------- description : str, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- str, formatted in JSON with keys: description : str n : sample number in this burst mux : XXX, multiplexer pin pair the voltage reading was taken with v : float, voltage measurement """ data_list = [] for m in range(n): data_list.append( self.json_writer.publish([ description, m, self.bin_mux[self.mux_value], self.voltage() ])) if n == 1: return data_list[0] if delay is not None: time.sleep(delay) return data_list def write(self, description='NA', n=1, delay=None): """Get ADC output and save to file, formatted as either .csv or .json. Parameters ---------- description : str, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- None, writes to disk the following data: description : str n : sample number in this burst mux : XXX, multiplexer pin pair the voltage reading was taken with v : float, voltage measurement """ wr = { "csv": self.csv_writer, "json": self.json_writer }[self.writer_output] for m in range(n): wr.write( [description, m, self.bin_mux[self.mux_value], self.voltage()]) if delay is not None: time.sleep(delay)
class mpu6050: # Global Variables GRAVITIY_MS2 = 9.80665 # State Variables accel_range = None gyro_range = None # Scale Modifiers ACCEL_SCALE_MODIFIER_2G = 16384.0 ACCEL_SCALE_MODIFIER_4G = 8192.0 ACCEL_SCALE_MODIFIER_8G = 4096.0 ACCEL_SCALE_MODIFIER_16G = 2048.0 GYRO_SCALE_MODIFIER_250DEG = 131.0 GYRO_SCALE_MODIFIER_500DEG = 65.5 GYRO_SCALE_MODIFIER_1000DEG = 32.8 GYRO_SCALE_MODIFIER_2000DEG = 16.4 # Pre-defined ranges ACCEL_RANGE_2G = 0x00 ACCEL_RANGE_4G = 0x08 ACCEL_RANGE_8G = 0x10 ACCEL_RANGE_16G = 0x18 GYRO_RANGE_250DEG = 0x00 GYRO_RANGE_500DEG = 0x08 GYRO_RANGE_1000DEG = 0x10 GYRO_RANGE_2000DEG = 0x18 # MPU-6050 Registers PWR_MGMT_1 = 0x6B PWR_MGMT_2 = 0x6C ACCEL_XOUT0 = 0x3B ACCEL_YOUT0 = 0x3D ACCEL_ZOUT0 = 0x3F TEMP_OUT0 = 0x41 GYRO_XOUT0 = 0x43 GYRO_YOUT0 = 0x45 GYRO_ZOUT0 = 0x47 ACCEL_CONFIG = 0x1C GYRO_CONFIG = 0x1B def __init__(self, bus_n, bus_addr=0x68, output='csv', name='MPU6050'): # i2c bus self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr) # Wake up the MPU-6050 since it starts in sleep mode # by toggling bit6 from 1 to 0, see pg 40 of RM-MPU-6000A-00 v4.2 self.bus.write_register_8bit(self.PWR_MGMT_1, 0x00) # information about this device self.metadata = Meta(name=name) self.metadata.description = 'TDK InvenSense Gyro & Accelerometer' self.metadata.urls = 'https://www.invensense.com/products/motion-tracking/6-axis/mpu-6050/' self.metadata.manufacturer = 'Adafruit Industries & TDK' # note: accuracy in datasheet is relative to scale factor - LSB/(deg/s) +/-3% # is there a better way to describe this? +/-3% below implies relative to deg/s output... self.metadata.header = [ 'description', 'sample_n', 'ax', 'ay', 'az', 'gx', 'gy', 'gz' ] self.metadata.dtype = [ 'str', 'int', 'float', 'float', 'float', 'float', 'float', 'float' ] self.metadata.units = [ None, 'count', 'g', 'g', 'g', 'deg/s', 'deg/s', 'deg/s' ] self.metadata.accuracy = [ None, 1, '+/-3%', '+/-3%', '+/-3%', '+/-3%', '+/-3%', '+/-3%' ] self.metadata.accuracy_precision_note = 'See datasheet for scale factor dependent accuracy & LSB precision' self.metadata.precision = None # specific specifications self.metadata.gyro_accuracy = '+/-3%, +/-2% cross axis' self.metadata.gyro_precision = '16bit' self.metadata.gyro_noise = '0.05 deg/s-rms' self.metadata.accel_accuracy = '+/-0.5%, +/-2 cross axis' self.metadata.accel_precision = '16bit' self.metadata.accel_noise = 'PSD 400 ug / Hz**1/2' self.metadata.bus_n = bus_n self.metadata.bus_addr = hex(bus_addr) # data recording classes self.writer_output = output self.csv_writer = CSVWriter(metadata=self.metadata, time_format='std_time_ms') self.json_writer = JSONWriter(metadata=self.metadata, time_format='std_time_ms') # I2C communication methods def read_i2c_word(self, register): """Read two i2c registers and combine them. register -- the first register to read from. Returns the combined read results. """ value = self.bus.read_register_16bit(register) if value >= 0x8000: return -((65535 - value) + 1) else: return value # MPU-6050 Methods def get_temp(self): """Reads the temperature from the onboard temperature sensor of the MPU-6050. Returns the temperature in degrees Celcius. """ raw_temp = self.read_i2c_word(self.TEMP_OUT0) # Get the actual temperature using the formule given in the # MPU-6050 Register Map and Descriptions revision 4.2, page 30 actual_temp = (raw_temp / 340.0) + 36.53 return actual_temp def set_accel_range(self, accel_range): """Sets the range of the accelerometer to range. accel_range -- the range to set the accelerometer to. Using a pre-defined range is advised. """ self.accel_range = accel_range # First change it to 0x00 to make sure we write the correct value later self.bus.write_register_16bit(self.ACCEL_CONFIG, 0x00) # Write the new range to the ACCEL_CONFIG register self.bus.write_register_16bit(self.ACCEL_CONFIG, accel_range) def read_accel_range(self, raw=False): """Reads the range the accelerometer is set to. If raw is True, it will return the raw value from the ACCEL_CONFIG register If raw is False, it will return an integer: -1, 2, 4, 8 or 16. When it returns -1 something went wrong. """ raw_data = self.bus.read_register_16bit(self.ACCEL_CONFIG) if raw is True: return raw_data elif raw is False: if raw_data == self.ACCEL_RANGE_2G: return 2 elif raw_data == self.ACCEL_RANGE_4G: return 4 elif raw_data == self.ACCEL_RANGE_8G: return 8 elif raw_data == self.ACCEL_RANGE_16G: return 16 else: return -1 def get_accel(self, g=False): """Gets and returns the X, Y and Z values from the accelerometer. If g is True, it will return the data in g If g is False, it will return the data in m/s^2 Returns a dictionary with the measurement results. """ x = self.bus.read_register_16bit(self.ACCEL_XOUT0) y = self.bus.read_register_16bit(self.ACCEL_YOUT0) z = self.bus.read_register_16bit(self.ACCEL_ZOUT0) accel_scale_modifier = None accel_range = self.read_accel_range(True) if accel_range == self.ACCEL_RANGE_2G: accel_scale_modifier = self.ACCEL_SCALE_MODIFIER_2G elif accel_range == self.ACCEL_RANGE_4G: accel_scale_modifier = self.ACCEL_SCALE_MODIFIER_4G elif accel_range == self.ACCEL_RANGE_8G: accel_scale_modifier = self.ACCEL_SCALE_MODIFIER_8G elif accel_range == self.ACCEL_RANGE_16G: accel_scale_modifier = self.ACCEL_SCALE_MODIFIER_16G else: print( "Unkown range - accel_scale_modifier set to self.ACCEL_SCALE_MODIFIER_2G" ) accel_scale_modifier = self.ACCEL_SCALE_MODIFIER_2G x = x / accel_scale_modifier y = y / accel_scale_modifier z = z / accel_scale_modifier if g is True: return {'x': x, 'y': y, 'z': z} elif g is False: x = x * self.GRAVITIY_MS2 y = y * self.GRAVITIY_MS2 z = z * self.GRAVITIY_MS2 return x, y, z def set_gyro_range(self, gyro_range): """Sets the range of the gyroscope to range. gyro_range -- the range to set the gyroscope to. Using a pre-defined range is advised. """ self.gyro_range = gyro_range # First change it to 0x00 to make sure we write the correct value later self.bus.write_register_16bit(self.GYRO_CONFIG, 0x00) # Write the new range to the ACCEL_CONFIG register self.bus.write_register_16bit(self.GYRO_CONFIG, gyro_range) def read_gyro_range(self, raw=False): """Reads the range the gyroscope is set to. If raw is True, it will return the raw value from the GYRO_CONFIG register. If raw is False, it will return 250, 500, 1000, 2000 or -1. If the returned value is equal to -1 something went wrong. """ raw_data = self.bus.read_register_16bit(self.GYRO_CONFIG) if raw is True: return raw_data elif raw is False: if raw_data == self.GYRO_RANGE_250DEG: return 250 elif raw_data == self.GYRO_RANGE_500DEG: return 500 elif raw_data == self.GYRO_RANGE_1000DEG: return 1000 elif raw_data == self.GYRO_RANGE_2000DEG: return 2000 else: return -1 def get_gyro(self): """Gets and returns the X, Y and Z values from the gyroscope. Returns the read values in a dictionary. """ x = self.read_i2c_word(self.GYRO_XOUT0) y = self.read_i2c_word(self.GYRO_YOUT0) z = self.read_i2c_word(self.GYRO_ZOUT0) gyro_scale_modifier = None gyro_range = self.read_gyro_range(True) if gyro_range == self.GYRO_RANGE_250DEG: gyro_scale_modifier = self.GYRO_SCALE_MODIFIER_250DEG elif gyro_range == self.GYRO_RANGE_500DEG: gyro_scale_modifier = self.GYRO_SCALE_MODIFIER_500DEG elif gyro_range == self.GYRO_RANGE_1000DEG: gyro_scale_modifier = self.GYRO_SCALE_MODIFIER_1000DEG elif gyro_range == self.GYRO_RANGE_2000DEG: gyro_scale_modifier = self.GYRO_SCALE_MODIFIER_2000DEG else: print( "Unkown range - gyro_scale_modifier set to self.GYRO_SCALE_MODIFIER_250DEG" ) gyro_scale_modifier = self.GYRO_SCALE_MODIFIER_250DEG x = x / gyro_scale_modifier y = y / gyro_scale_modifier z = z / gyro_scale_modifier return x, y, z def get_all(self): """Reads and returns all the available data.""" temp = self.get_temp() accel = self.get_accel() gyro = self.get_gyro() return [temp] + list(accel) + list(gyro) def get(self, description='NA', n=1, delay=None): """Get formatted output. Parameters ---------- description : char, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- data : list, data containing: description: str, description of sample under test temperature : float, temperature in degrees Celcius delay : float, seconds to delay between samples if n > 1 """ data_list = [] for m in range(1, n + 1): data_list.append([description, m] + list(self.get_accel()) + list(self.get_gyro())) if n == 1: return data_list[0] if delay is not None: time.sleep(delay) return data_list def publish(self, description='NA', n=1, delay=None): """Output relay status data in JSON. Parameters ---------- description : str, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- str, formatted in JSON with keys: description: str, description of sample under test temperature : float, temperature in degrees Celcius """ data_list = [] for m in range(n): data_list.append( self.json_writer.publish([description, m] + list(self.get_accel()) + list(self.get_gyro()))) if n == 1: return data_list[0] if delay is not None: time.sleep(delay) return data_list def write(self, description='NA', n=1, delay=None): """Format output and save to file, formatted as either .csv or .json. Parameters ---------- description : str, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- None, writes to disk the following data: description : str, description of sample sample_n : int, sample number in this burst temperature : float, temperature in degrees Celcius """ wr = { "csv": self.csv_writer, "json": self.json_writer }[self.writer_output] for m in range(n): wr.write([description, m] + list(self.get_accel()) + list(self.get_gyro())) if delay is not None: time.sleep(delay)
class DS3231: def __init__(self, bus_n, bus_addr=0x68, output='csv', name='DS3231'): """Initialize worker device on i2c bus. Parameters ---------- bus_n : int, i2c bus number on Controller bus_addr : int, i2c bus number of this Worker device output : str, output data format, either 'csv' (default) or 'json' """ # i2c bus self.bus = I2C(bus_n=bus_n, bus_addr=bus_addr) # information about this device self.metadata = Meta(name=name) self.metadata.description = 'Adafruit DS3221 Precision RTC' self.metadata.urls = 'https://datasheets.maximintegrated.com/en/ds/DS3231.pdf' self.metadata.manufacturer = 'Adafruit Industries' self.metadata.header = [ "description", "sample_n", "rtc_time", "temp_C" ] self.metadata.dtype = ['str', 'int', 'str', 'float'] self.metadata.units = [None, 'count', 'datetime', 'degrees Celcius'] self.metadata.accuracy = [None, 1, '+/- 3.5 ppm', '+/- 3.0'] self.metadata.precision = [None, 1, '1 second', 0.25] self.metadata.bus_n = bus_n self.metadata.bus_addr = hex(bus_addr) # python strftime specification for RTC output precision self.metadata.rtc_time_format = '%Y-%m-%d %H:%M:%S' # data recording method # note: using millisecond accuracy on driver timestamp, even though # RTC is only 1 second resolution self.writer_output = output self.csv_writer = CSVWriter(metadata=self.metadata, time_format='std_time_ms') self.json_writer = JSONWriter(metadata=self.metadata, time_format='std_time_ms') def set_time(self, YY, MM, DD, hh, mm, ss, micro, tz): """Set time of RTC Parameters ---------- YY : int, year range 1900 to 2100 (approx) MM : int, month, range 1 to 12 DD : int, day, range 1 to 31 hh : int, hour, range 0 to 23 (24hr clock) or 1-12 (12 hr clock) mm : int, minute, range 1-59 ss : int, second, range 1-59 micro : int, microseconds, range 0-999 (not implemented) tz : str, time zone (not implemented) """ self.bus.write_register_8bit(reg_addr=0x00, data=dec2bcd(ss)) self.bus.write_register_8bit(reg_addr=0x01, data=dec2bcd(mm)) self.bus.write_register_8bit(reg_addr=0x02, data=dec2bcd(hh)) self.bus.write_register_8bit(reg_addr=0x04, data=dec2bcd(DD)) if YY >= 2000: MM = dec2bcd(MM) | 0b10000000 YY = dec2bcd(YY - 2000) else: MM = dec2bcd(MM) YY = dec2bcd(YY - 1900) self.bus.write_register_8bit(reg_addr=0x05, data=MM) self.bus.write_register_8bit(reg_addr=0x06, data=YY) def get_time(self): """Get time from RTC Returns ---------- YY : int, year range 1900 to 2100 (approx) MM : int, month, range 1 to 12 DD : int, day, range 1 to 31 hh : int, hour, range 0 to 23 (24hr clock) or 1-12 (12 hr clock) mm : int, minute, range 1-59 ss : int, second, range 1-59 """ data = self.bus.read_register_nbit(reg_addr=0x00, n=7) ss = bcd2dec(data[0] & 0b01111111) mm = bcd2dec(data[1] & 0b01111111) if data[2] & 0b01000000 > 0: hh = bcd2dec(data[2] & 0b00011111) if data[2] & 0b00100000 > 0: hh += 12 else: hh = bcd2dec(data[2] & 0b00111111) DD = bcd2dec(data[4] & 0b00111111) MM = bcd2dec(data[5] & 0b00011111) YY = bcd2dec(data[6]) if data[5] & 0b10000000 > 0: YY = YY + 2000 else: YY = YY + 1900 return YY, MM, DD, hh, mm, ss def get_temp(self): """Get temperature of RTC Returns ------- float, temperature in degrees Celsius """ data = self.bus.read_register_nbit(reg_addr=0x11, n=2) return float(data[0]) + (data[1] >> 6) * 0.25 def publish(self, description='NA', n=1, delay=None): """Get RTC time and temperature in JSON, plus metadata at intervals set by self.metadata_interval Parameters ---------- description : char, description of data sample collected, default='NA' n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- str, formatted in JSON with keys: description : str n : sample number in this burst std_time : str, time formatted in YY-MM-DD hh:mm:ss temp_C : float, temperature of RTC in degrees C """ data_list = [] for m in range(n): std_time = '{:02d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}'.format( *self.get_time()) temp_C = self.get_temp() data_list.append( self.json_writer.publish([description, 0, std_time, temp_C])) if n == 1: return data_list[0] if delay is not None: time.sleep(delay) return data_list def write(self, description='NA', n=1, delay=None): """Get RTC time and temperature and save to file, formatted as either .csv or .json. Parameters ---------- description : str, description of data sample collected n : int, number of samples to record in this burst delay : float, seconds to delay between samples if n > 1 Returns ------- None, writes to disk the following data: description : str n : sample number in this burst std_time : str, time formatted in YY-MM-DD hh:mm:ss temp_C : float, temperature of RTC in degrees C """ wr = { "csv": self.csv_writer, "json": self.json_writer }[self.writer_output] for m in range(n): std_time = '{:02d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}'.format( *self.get_time()) temp_C = self.get_temp() wr.write([description, m, std_time, temp_C]) if delay is not None: time.sleep(delay)