diff --git a/app.py b/app.py index 85b903f9f2db4ccf09896fc241dde1cefe91a9b0..4ae73b7bfbaa258b259c1ddd1f34f35aac079f20 100644 --- a/app.py +++ b/app.py @@ -12,8 +12,8 @@ st.title("Real-Time Sensor Data Dashboard") with st.form("mqtt_form"): MQTT_HOST = st.text_input("Enter your MQTT host:", "b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud") MQTT_PORT = st.number_input("Enter the port number:", min_value=1, max_value=65535, value=8883) - MQTT_USERNAME = st.text_input("Enter your MQTT username:", "Luthiraa") - MQTT_PASSWORD = st.text_input("Enter your MQTT password:", "theboss1010", type="password") + MQTT_USERNAME = st.text_input("Enter your MQTT username:", "LuthiraMQ") + MQTT_PASSWORD = st.text_input("Enter your MQTT password:", "jLVx8y9v83gmgERTr0AP", type="password") MQTT_TOPIC = st.text_input("Enter your MQTT topic:", "sensors/bme680/data") submit_button = st.form_submit_button(label="Submit") diff --git a/scripts/adafruit_bme680.mpy b/scripts/adafruit_bme680.mpy new file mode 100644 index 0000000000000000000000000000000000000000..0e0d14c46498b6128cdf05e935f856080d23fab7 Binary files /dev/null and b/scripts/adafruit_bme680.mpy differ diff --git a/scripts/adafruit_bme680.py b/scripts/adafruit_bme680.py new file mode 100644 index 0000000000000000000000000000000000000000..7c7d5d4bbbc4b4ee3b2871ed8e5f51f7b2fb7dc3 --- /dev/null +++ b/scripts/adafruit_bme680.py @@ -0,0 +1,769 @@ +# SPDX-FileCopyrightText: 2017 ladyada for Adafruit Industries +# +# SPDX-License-Identifier: MIT AND BSD-3-Clause + + +""" +`adafruit_bme680` +================================================================================ + +CircuitPython library for BME680 temperature, pressure and humidity sensor. + + +* Author(s): Limor Fried, William Garber, many others + + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit BME680 Temp, Humidity, Pressure and Gas Sensor `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases +* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +""" + +import math +import struct +import time + +from micropython import const + + +def delay_microseconds(nusec): + """HELP must be same as dev->delay_us""" + time.sleep(nusec / 1000000.0) + + +try: + # Used only for type annotations. + + import typing + + from busio import I2C, SPI + from circuitpython_typing import ReadableBuffer + from digitalio import DigitalInOut + +except ImportError: + pass + +__version__ = "3.7.9" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BME680.git" + + +# I2C ADDRESS/BITS/SETTINGS NEW +# ----------------------------------------------------------------------- +_BME68X_ENABLE_HEATER = const(0x00) +_BME68X_DISABLE_HEATER = const(0x01) +_BME68X_DISABLE_GAS_MEAS = const(0x00) +_BME68X_ENABLE_GAS_MEAS_L = const(0x01) +_BME68X_ENABLE_GAS_MEAS_H = const(0x02) +_BME68X_SLEEP_MODE = const(0) +_BME68X_FORCED_MODE = const(1) +_BME68X_VARIANT_GAS_LOW = const(0x00) +_BME68X_VARIANT_GAS_HIGH = const(0x01) +_BME68X_HCTRL_MSK = const(0x08) +_BME68X_HCTRL_POS = const(3) +_BME68X_NBCONV_MSK = const(0x0F) +_BME68X_RUN_GAS_MSK = const(0x30) +_BME68X_RUN_GAS_POS = const(4) +_BME68X_MODE_MSK = const(0x03) +_BME68X_PERIOD_POLL = const(10000) +_BME68X_REG_CTRL_GAS_0 = const(0x70) +_BME68X_REG_CTRL_GAS_1 = const(0x71) + +# I2C ADDRESS/BITS/SETTINGS +# ----------------------------------------------------------------------- +_BME680_CHIPID = const(0x61) + +_BME680_REG_CHIPID = const(0xD0) +_BME68X_REG_VARIANT = const(0xF0) +_BME680_BME680_COEFF_ADDR1 = const(0x89) +_BME680_BME680_COEFF_ADDR2 = const(0xE1) +_BME680_BME680_RES_HEAT_0 = const(0x5A) +_BME680_BME680_GAS_WAIT_0 = const(0x64) + +_BME680_REG_SOFTRESET = const(0xE0) +_BME680_REG_CTRL_GAS = const(0x71) +_BME680_REG_CTRL_HUM = const(0x72) +_BME680_REG_STATUS = const(0x73) +_BME680_REG_CTRL_MEAS = const(0x74) +_BME680_REG_CONFIG = const(0x75) + +_BME680_REG_MEAS_STATUS = const(0x1D) +_BME680_REG_PDATA = const(0x1F) +_BME680_REG_TDATA = const(0x22) +_BME680_REG_HDATA = const(0x25) + +_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16) +_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127) + +_BME680_RUNGAS = const(0x10) + +_LOOKUP_TABLE_1 = ( + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2126008810.0, + 2147483647.0, + 2130303777.0, + 2147483647.0, + 2147483647.0, + 2143188679.0, + 2136746228.0, + 2147483647.0, + 2126008810.0, + 2147483647.0, + 2147483647.0, +) + +_LOOKUP_TABLE_2 = ( + 4096000000.0, + 2048000000.0, + 1024000000.0, + 512000000.0, + 255744255.0, + 127110228.0, + 64000000.0, + 32258064.0, + 16016016.0, + 8000000.0, + 4000000.0, + 2000000.0, + 1000000.0, + 500000.0, + 250000.0, + 125000.0, +) + + +def bme_set_bits(reg_data, bitname_msk, bitname_pos, data): + """ + Macro to set bits + data2 = data << bitname_pos + set masked bits from data2 in reg_data + """ + return (reg_data & ~bitname_msk) | ((data << bitname_pos) & bitname_msk) + + +def bme_set_bits_pos_0(reg_data, bitname_msk, data): + """ + Macro to set bits starting from position 0 + set masked bits from data in reg_data + """ + return (reg_data & ~bitname_msk) | (data & bitname_msk) + + +def _read24(arr: ReadableBuffer) -> float: + """Parse an unsigned 24-bit value as a floating point and return it.""" + ret = 0.0 + # print([hex(i) for i in arr]) + for b in arr: + ret *= 256.0 + ret += float(b & 0xFF) + return ret + + +class Adafruit_BME680: + """Driver from BME680 air quality sensor + + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading.""" + + def __init__(self, *, refresh_rate: int = 10) -> None: + """Check the BME680 was found, read the coefficients and enable the sensor for continuous + reads.""" + self._write(_BME680_REG_SOFTRESET, [0xB6]) + time.sleep(0.005) + + # Check device ID. + chip_id = self._read_byte(_BME680_REG_CHIPID) + if chip_id != _BME680_CHIPID: + raise RuntimeError("Failed to find BME680! Chip ID 0x%x" % chip_id) + + # Get variant + self._chip_variant = self._read_byte(_BME68X_REG_VARIANT) + + self._read_calibration() + + # set up heater + self._write(_BME680_BME680_RES_HEAT_0, [0x73]) + self._write(_BME680_BME680_GAS_WAIT_0, [0x65]) + + self.sea_level_pressure = 1013.25 + """Pressure in hectoPascals at sea level. Used to calibrate :attr:`altitude`.""" + + # Default oversampling and filter register values. + self._pressure_oversample = 0b011 + self._temp_oversample = 0b100 + self._humidity_oversample = 0b010 + self._filter = 0b010 + + # Gas measurements, as a mask applied to _BME680_RUNGAS + self._run_gas = 0xFF + + self._adc_pres = None + self._adc_temp = None + self._adc_hum = None + self._adc_gas = None + self._gas_range = None + self._t_fine = None + + self._last_reading = 0 + self._min_refresh_time = 1 / refresh_rate + + self._amb_temp = 25 # Copy required parameters from reference bme68x_dev struct + self.set_gas_heater(320, 150) # heater 320 deg C for 150 msec + + @property + def pressure_oversample(self) -> int: + """The oversampling for pressure sensor""" + return _BME680_SAMPLERATES[self._pressure_oversample] + + @pressure_oversample.setter + def pressure_oversample(self, sample_rate: int) -> None: + if sample_rate in _BME680_SAMPLERATES: + self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def humidity_oversample(self) -> int: + """The oversampling for humidity sensor""" + return _BME680_SAMPLERATES[self._humidity_oversample] + + @humidity_oversample.setter + def humidity_oversample(self, sample_rate: int) -> None: + if sample_rate in _BME680_SAMPLERATES: + self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def temperature_oversample(self) -> int: + """The oversampling for temperature sensor""" + return _BME680_SAMPLERATES[self._temp_oversample] + + @temperature_oversample.setter + def temperature_oversample(self, sample_rate: int) -> None: + if sample_rate in _BME680_SAMPLERATES: + self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def filter_size(self) -> int: + """The filter size for the built in IIR filter""" + return _BME680_FILTERSIZES[self._filter] + + @filter_size.setter + def filter_size(self, size: int) -> None: + if size in _BME680_FILTERSIZES: + self._filter = _BME680_FILTERSIZES.index(size) + else: + raise RuntimeError("Invalid size") + + @property + def temperature(self) -> float: + """The compensated temperature in degrees Celsius.""" + self._perform_reading() + calc_temp = ((self._t_fine * 5) + 128) / 256 + return calc_temp / 100 + + @property + def pressure(self) -> float: + """The barometric pressure in hectoPascals""" + self._perform_reading() + 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 + return calc_pres / 100 + + @property + def relative_humidity(self) -> float: + """The relative humidity in RH %""" + return self.humidity + + @property + def humidity(self) -> float: + """The relative humidity in RH %""" + self._perform_reading() + 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 + + calc_hum = min(calc_hum, 100) + calc_hum = max(calc_hum, 0) + return calc_hum + + @property + def altitude(self) -> float: + """The altitude based on current :attr:`pressure` vs the sea level pressure + (:attr:`sea_level_pressure`) - which you must enter ahead of time)""" + pressure = self.pressure # in Si units for hPascal + return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) + + @property + def gas(self) -> int: + """The gas resistance in ohms""" + self._perform_reading() + if self._chip_variant == 0x01: + # taken from https://github.com/BoschSensortec/BME68x-Sensor-API + var1 = 262144 >> self._gas_range + var2 = self._adc_gas - 512 + var2 *= 3 + var2 = 4096 + var2 + calc_gas_res = (10000 * var1) / var2 + calc_gas_res = calc_gas_res * 100 + else: + var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536 + var2 = ((self._adc_gas * 32768) - 16777216) + var1 + var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 + calc_gas_res = (var3 + (var2 / 2)) / var2 + return int(calc_gas_res) + + def _perform_reading(self) -> None: + """Perform a single-shot reading from the sensor and fill internal data structure for + calculations""" + if time.monotonic() - self._last_reading < self._min_refresh_time: + return + + # set filter + self._write(_BME680_REG_CONFIG, [self._filter << 2]) + # turn on temp oversample & pressure oversample + self._write( + _BME680_REG_CTRL_MEAS, + [(self._temp_oversample << 5) | (self._pressure_oversample << 2)], + ) + # turn on humidity oversample + self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) + # gas measurements enabled + if self._chip_variant == 0x01: + self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS) << 1]) + else: + self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS)]) + ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) + ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! + self._write(_BME680_REG_CTRL_MEAS, [ctrl]) + new_data = False + while not new_data: + data = self._read(_BME680_REG_MEAS_STATUS, 17) + new_data = data[0] & 0x80 != 0 + time.sleep(0.005) + self._last_reading = time.monotonic() + + self._adc_pres = _read24(data[2:5]) / 16 + self._adc_temp = _read24(data[5:8]) / 16 + self._adc_hum = struct.unpack(">H", bytes(data[8:10]))[0] + if self._chip_variant == 0x01: + self._adc_gas = int(struct.unpack(">H", bytes(data[15:17]))[0] / 64) + self._gas_range = data[16] & 0x0F + else: + self._adc_gas = int(struct.unpack(">H", bytes(data[13:15]))[0] / 64) + self._gas_range = data[14] & 0x0F + + 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) + + def _read_calibration(self) -> None: + """Read & save the calibration coefficients""" + coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) + coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) + + coeff = list(struct.unpack(" int: + """Read a byte register value and return it""" + return self._read(register, 1)[0] + + def _read(self, register: int, length: int) -> bytearray: + raise NotImplementedError() + + def _write(self, register: int, values: bytearray) -> None: + raise NotImplementedError() + + def set_gas_heater(self, heater_temp: int, heater_time: int) -> bool: + """ + Enable and configure gas reading + heater (None disables) + :param heater_temp: Desired temperature in degrees Centigrade + :param heater_time: Time to keep heater on in milliseconds + :return: True on success, False on failure + """ + try: + if (heater_temp is None) or (heater_time is None): + self._set_heatr_conf(heater_temp or 0, heater_time or 0, enable=False) + else: + self._set_heatr_conf(heater_temp, heater_time) + except OSError: + return False + return True + + def _set_heatr_conf(self, heater_temp: int, heater_time: int, enable: bool = True) -> None: + # restrict to BME68X_FORCED_MODE + op_mode: int = _BME68X_FORCED_MODE + nb_conv: int = 0 + hctrl: int = _BME68X_ENABLE_HEATER + run_gas: int = 0 + ctrl_gas_data_0: int = 0 + ctrl_gas_data_1: int = 0 + + self._set_op_mode(_BME68X_SLEEP_MODE) + self._set_conf(heater_temp, heater_time, op_mode) + ctrl_gas_data_0 = self._read_byte(_BME68X_REG_CTRL_GAS_0) + ctrl_gas_data_1 = self._read_byte(_BME68X_REG_CTRL_GAS_1) + if enable: + hctrl = _BME68X_ENABLE_HEATER + if self._chip_variant == _BME68X_VARIANT_GAS_HIGH: + run_gas = _BME68X_ENABLE_GAS_MEAS_H + else: + run_gas = _BME68X_ENABLE_GAS_MEAS_L + else: + hctrl = _BME68X_DISABLE_HEATER + run_gas = _BME68X_DISABLE_GAS_MEAS + self._run_gas = ~(run_gas - 1) + + ctrl_gas_data_0 = bme_set_bits(ctrl_gas_data_0, _BME68X_HCTRL_MSK, _BME68X_HCTRL_POS, hctrl) + ctrl_gas_data_1 = bme_set_bits_pos_0(ctrl_gas_data_1, _BME68X_NBCONV_MSK, nb_conv) + ctrl_gas_data_1 = bme_set_bits( + ctrl_gas_data_1, _BME68X_RUN_GAS_MSK, _BME68X_RUN_GAS_POS, run_gas + ) + self._write(_BME68X_REG_CTRL_GAS_0, [ctrl_gas_data_0]) + self._write(_BME68X_REG_CTRL_GAS_1, [ctrl_gas_data_1]) + + def _set_op_mode(self, op_mode: int) -> None: + """ + * @brief This API is used to set the operation mode of the sensor + """ + tmp_pow_mode: int = 0 + pow_mode: int = _BME68X_FORCED_MODE + # Call until in sleep + + # was a do {} while() loop + while pow_mode != _BME68X_SLEEP_MODE: + tmp_pow_mode = self._read_byte(_BME680_REG_CTRL_MEAS) + # Put to sleep before changing mode + pow_mode = tmp_pow_mode & _BME68X_MODE_MSK + if pow_mode != _BME68X_SLEEP_MODE: + tmp_pow_mode &= ~_BME68X_MODE_MSK # Set to sleep + self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode]) + # dev->delay_us(_BME68X_PERIOD_POLL, dev->intf_ptr) # HELP + delay_microseconds(_BME68X_PERIOD_POLL) + # Already in sleep + if op_mode != _BME68X_SLEEP_MODE: + tmp_pow_mode = (tmp_pow_mode & ~_BME68X_MODE_MSK) | (op_mode & _BME68X_MODE_MSK) + self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode]) + + def _set_conf(self, heater_temp: int, heater_time: int, op_mode: int) -> None: + """ + This internal API is used to set heater configurations + """ + + if op_mode != _BME68X_FORCED_MODE: + raise OSError("GasHeaterException: _set_conf not forced mode") + rh_reg_data: int = self._calc_res_heat(heater_temp) + gw_reg_data: int = self._calc_gas_wait(heater_time) + self._write(_BME680_BME680_RES_HEAT_0, [rh_reg_data]) + self._write(_BME680_BME680_GAS_WAIT_0, [gw_reg_data]) + + def _calc_res_heat(self, temp: int) -> int: + """ + This internal API is used to calculate the heater resistance value using float + """ + gh1: int = self._gas_calibration[0] + gh2: int = self._gas_calibration[1] + gh3: int = self._gas_calibration[2] + htr: int = self._heat_range + htv: int = self._heat_val + amb: int = self._amb_temp + + temp = min(temp, 400) # Cap temperature + + var1: int = ((int(amb) * gh3) / 1000) * 256 + var2: int = (gh1 + 784) * (((((gh2 + 154009) * temp * 5) / 100) + 3276800) / 10) + var3: int = var1 + (var2 / 2) + var4: int = var3 / (htr + 4) + var5: int = (131 * htv) + 65536 + heatr_res_x100: int = int(((var4 / var5) - 250) * 34) + heatr_res: int = int((heatr_res_x100 + 50) / 100) + + return heatr_res + + def _calc_res_heat(self, temp: int) -> int: + """ + This internal API is used to calculate the heater resistance value + """ + gh1: float = float(self._gas_calibration[0]) + gh2: float = float(self._gas_calibration[1]) + gh3: float = float(self._gas_calibration[2]) + htr: float = float(self._heat_range) + htv: float = float(self._heat_val) + amb: float = float(self._amb_temp) + + temp = min(temp, 400) # Cap temperature + + var1: float = (gh1 / (16.0)) + 49.0 + var2: float = ((gh2 / (32768.0)) * (0.0005)) + 0.00235 + var3: float = gh3 / (1024.0) + var4: float = var1 * (1.0 + (var2 * float(temp))) + var5: float = var4 + (var3 * amb) + res_heat: int = int(3.4 * ((var5 * (4 / (4 + htr)) * (1 / (1 + (htv * 0.002)))) - 25)) + return res_heat + + def _calc_gas_wait(self, dur: int) -> int: + """ + This internal API is used to calculate the gas wait + """ + factor: int = 0 + durval: int = 0xFF # Max duration + + if dur >= 0xFC0: + return durval + while dur > 0x3F: + dur = dur / 4 + factor += 1 + durval = int(dur + (factor * 64)) + return durval + + +class Adafruit_BME680_I2C(Adafruit_BME680): + """Driver for I2C connected BME680. + + :param ~busio.I2C i2c: The I2C bus the BME680 is connected to. + :param int address: I2C device address. Defaults to :const:`0x77` + :param bool debug: Print debug statements when `True`. Defaults to `False` + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading. + + **Quickstart: Importing and using the BME680** + + Here is an example of using the :class:`BMP680_I2C` class. + First you will need to import the libraries to use the sensor + + .. code-block:: python + + import board + import adafruit_bme680 + + Once this is done you can define your ``board.I2C`` object and define your sensor object + + .. code-block:: python + + i2c = board.I2C() # uses board.SCL and board.SDA + bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c) + + You need to setup the pressure at sea level + + .. code-block:: python + + bme680.sea_level_pressure = 1013.25 + + Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`, + :attr:`pressure` and :attr:`altitude` attributes + + .. code-block:: python + + temperature = bme680.temperature + gas = bme680.gas + relative_humidity = bme680.relative_humidity + pressure = bme680.pressure + altitude = bme680.altitude + + """ + + def __init__( + self, + i2c: I2C, + address: int = 0x77, + debug: bool = False, + *, + refresh_rate: int = 10, + ) -> None: + """Initialize the I2C device at the 'address' given""" + from adafruit_bus_device import ( + i2c_device, + ) + + self._i2c = i2c_device.I2CDevice(i2c, address) + self._debug = debug + super().__init__(refresh_rate=refresh_rate) + + def _read(self, register: int, length: int) -> bytearray: + """Returns an array of 'length' bytes from the 'register'""" + with self._i2c as i2c: + i2c.write(bytes([register & 0xFF])) + result = bytearray(length) + i2c.readinto(result) + if self._debug: + print(f"\t${register:02X} => {[hex(i) for i in result]}") + return result + + def _write(self, register: int, values: ReadableBuffer) -> None: + """Writes an array of 'length' bytes to the 'register'""" + with self._i2c as i2c: + buffer = bytearray(2 * len(values)) + for i, value in enumerate(values): + buffer[2 * i] = register + i + buffer[2 * i + 1] = value + i2c.write(buffer) + if self._debug: + print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}") + + +class Adafruit_BME680_SPI(Adafruit_BME680): + """Driver for SPI connected BME680. + + :param ~busio.SPI spi: SPI device + :param ~digitalio.DigitalInOut cs: Chip Select + :param bool debug: Print debug statements when `True`. Defaults to `False` + :param int baudrate: Clock rate, default is :const:`100000` + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading. + + + **Quickstart: Importing and using the BME680** + + Here is an example of using the :class:`BMP680_SPI` class. + First you will need to import the libraries to use the sensor + + .. code-block:: python + + import board + from digitalio import DigitalInOut, Direction + import adafruit_bme680 + + Once this is done you can define your ``board.SPI`` object and define your sensor object + + .. code-block:: python + + cs = digitalio.DigitalInOut(board.D10) + spi = board.SPI() + bme680 = adafruit_bme680.Adafruit_BME680_SPI(spi, cs) + + You need to setup the pressure at sea level + + .. code-block:: python + + bme680.sea_level_pressure = 1013.25 + + Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`, + :attr:`pressure` and :attr:`altitude` attributes + + .. code-block:: python + + temperature = bme680.temperature + gas = bme680.gas + relative_humidity = bme680.relative_humidity + pressure = bme680.pressure + altitude = bme680.altitude + + """ + + def __init__( # noqa: PLR0913 Too many arguments in function definition + self, + spi: SPI, + cs: DigitalInOut, + baudrate: int = 100000, + debug: bool = False, + *, + refresh_rate: int = 10, + ) -> None: + from adafruit_bus_device import ( + spi_device, + ) + + self._spi = spi_device.SPIDevice(spi, cs, baudrate=baudrate) + self._debug = debug + super().__init__(refresh_rate=refresh_rate) + + def _read(self, register: int, length: int) -> bytearray: + if register != _BME680_REG_STATUS: + # _BME680_REG_STATUS exists in both SPI memory pages + # For all other registers, we must set the correct memory page + self._set_spi_mem_page(register) + + register = (register | 0x80) & 0xFF # Read single, bit 7 high. + with self._spi as spi: + spi.write(bytearray([register])) + result = bytearray(length) + spi.readinto(result) + if self._debug: + print(f"\t${register:02X} => {[hex(i) for i in result]}") + return result + + def _write(self, register: int, values: ReadableBuffer) -> None: + if register != _BME680_REG_STATUS: + # _BME680_REG_STATUS exists in both SPI memory pages + # For all other registers, we must set the correct memory page + self._set_spi_mem_page(register) + register &= 0x7F # Write, bit 7 low. + with self._spi as spi: + buffer = bytearray(2 * len(values)) + for i, value in enumerate(values): + buffer[2 * i] = register + i + buffer[2 * i + 1] = value & 0xFF + spi.write(buffer) + if self._debug: + print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}") + + def _set_spi_mem_page(self, register: int) -> None: + spi_mem_page = 0x00 + if register < 0x80: + spi_mem_page = 0x10 + self._write(_BME680_REG_STATUS, [spi_mem_page]) diff --git a/scripts/adafruit_bus_device/__init__.py b/scripts/adafruit_bus_device/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scripts/adafruit_bus_device/i2c_device.py b/scripts/adafruit_bus_device/i2c_device.py new file mode 100644 index 0000000000000000000000000000000000000000..c605290d305508c076242bc0c6f59216cfa05e92 --- /dev/null +++ b/scripts/adafruit_bus_device/i2c_device.py @@ -0,0 +1,187 @@ +# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_bus_device.i2c_device` - I2C Bus Device +==================================================== +""" + +import time + +try: + from typing import Optional, Type + from types import TracebackType + from circuitpython_typing import ReadableBuffer, WriteableBuffer + + # Used only for type annotations. + from busio import I2C +except ImportError: + pass + + +__version__ = "5.2.10" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BusDevice.git" + + +class I2CDevice: + """ + Represents a single I2C device and manages locking the bus and the device + address. + + :param ~busio.I2C i2c: The I2C bus the device is on + :param int device_address: The 7 bit device address + :param bool probe: Probe for the device upon object creation, default is true + + .. note:: This class is **NOT** built into CircuitPython. See + :ref:`here for install instructions `. + + Example: + + .. code-block:: python + + import busio + from board import * + from adafruit_bus_device.i2c_device import I2CDevice + + with busio.I2C(SCL, SDA) as i2c: + device = I2CDevice(i2c, 0x70) + bytes_read = bytearray(4) + with device: + device.readinto(bytes_read) + # A second transaction + with device: + device.write(bytes_read) + """ + + def __init__(self, i2c: I2C, device_address: int, probe: bool = True) -> None: + self.i2c = i2c + self.device_address = device_address + + if probe: + self.__probe_for_device() + + def readinto( + self, buf: WriteableBuffer, *, start: int = 0, end: Optional[int] = None + ) -> None: + """ + Read into ``buf`` from the device. The number of bytes read will be the + length of ``buf``. + + If ``start`` or ``end`` is provided, then the buffer will be sliced + as if ``buf[start:end]``. This will not cause an allocation like + ``buf[start:end]`` will so it saves memory. + + :param ~WriteableBuffer buffer: buffer to write into + :param int start: Index to start writing at + :param int end: Index to write up to but not include; if None, use ``len(buf)`` + """ + if end is None: + end = len(buf) + self.i2c.readfrom_into(self.device_address, buf, start=start, end=end) + + def write( + self, buf: ReadableBuffer, *, start: int = 0, end: Optional[int] = None + ) -> None: + """ + Write the bytes from ``buffer`` to the device, then transmit a stop + bit. + + If ``start`` or ``end`` is provided, then the buffer will be sliced + as if ``buffer[start:end]``. This will not cause an allocation like + ``buffer[start:end]`` will so it saves memory. + + :param ~ReadableBuffer buffer: buffer containing the bytes to write + :param int start: Index to start writing from + :param int end: Index to read up to but not include; if None, use ``len(buf)`` + """ + if end is None: + end = len(buf) + self.i2c.writeto(self.device_address, buf, start=start, end=end) + + # pylint: disable-msg=too-many-arguments + def write_then_readinto( + self, + out_buffer: ReadableBuffer, + in_buffer: WriteableBuffer, + *, + out_start: int = 0, + out_end: Optional[int] = None, + in_start: int = 0, + in_end: Optional[int] = None + ) -> None: + """ + Write the bytes from ``out_buffer`` to the device, then immediately + reads into ``in_buffer`` from the device. The number of bytes read + will be the length of ``in_buffer``. + + If ``out_start`` or ``out_end`` is provided, then the output buffer + will be sliced as if ``out_buffer[out_start:out_end]``. This will + not cause an allocation like ``buffer[out_start:out_end]`` will so + it saves memory. + + If ``in_start`` or ``in_end`` is provided, then the input buffer + will be sliced as if ``in_buffer[in_start:in_end]``. This will not + cause an allocation like ``in_buffer[in_start:in_end]`` will so + it saves memory. + + :param ~ReadableBuffer out_buffer: buffer containing the bytes to write + :param ~WriteableBuffer in_buffer: buffer containing the bytes to read into + :param int out_start: Index to start writing from + :param int out_end: Index to read up to but not include; if None, use ``len(out_buffer)`` + :param int in_start: Index to start writing at + :param int in_end: Index to write up to but not include; if None, use ``len(in_buffer)`` + """ + if out_end is None: + out_end = len(out_buffer) + if in_end is None: + in_end = len(in_buffer) + + self.i2c.writeto_then_readfrom( + self.device_address, + out_buffer, + in_buffer, + out_start=out_start, + out_end=out_end, + in_start=in_start, + in_end=in_end, + ) + + # pylint: enable-msg=too-many-arguments + + def __enter__(self) -> "I2CDevice": + while not self.i2c.try_lock(): + time.sleep(0) + return self + + def __exit__( + self, + exc_type: Optional[Type[type]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: + self.i2c.unlock() + return False + + def __probe_for_device(self) -> None: + """ + Try to read a byte from an address, + if you get an OSError it means the device is not there + or that the device does not support these means of probing + """ + while not self.i2c.try_lock(): + time.sleep(0) + try: + self.i2c.writeto(self.device_address, b"") + except OSError: + # some OS's dont like writing an empty bytesting... + # Retry by reading a byte + try: + result = bytearray(1) + self.i2c.readfrom_into(self.device_address, result) + except OSError: + # pylint: disable=raise-missing-from + raise ValueError("No I2C device at address: 0x%x" % self.device_address) + # pylint: enable=raise-missing-from + finally: + self.i2c.unlock() diff --git a/scripts/adafruit_bus_device/spi_device.py b/scripts/adafruit_bus_device/spi_device.py new file mode 100644 index 0000000000000000000000000000000000000000..60954e0c30d8d140a1a1550993eac30eb97931a1 --- /dev/null +++ b/scripts/adafruit_bus_device/spi_device.py @@ -0,0 +1,121 @@ +# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +# pylint: disable=too-few-public-methods + +""" +`adafruit_bus_device.spi_device` - SPI Bus Device +==================================================== +""" + +import time + +try: + from typing import Optional, Type + from types import TracebackType + + # Used only for type annotations. + from busio import SPI + from digitalio import DigitalInOut +except ImportError: + pass + + +__version__ = "5.2.10" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BusDevice.git" + + +class SPIDevice: + """ + Represents a single SPI device and manages locking the bus and the device + address. + + :param ~busio.SPI spi: The SPI bus the device is on + :param ~digitalio.DigitalInOut chip_select: The chip select pin object that implements the + DigitalInOut API. + :param bool cs_active_value: Set to True if your device requires CS to be active high. + Defaults to False. + :param int baudrate: The desired SCK clock rate in Hertz. The actual clock rate may be + higher or lower due to the granularity of available clock settings (MCU dependent). + :param int polarity: The base state of the SCK clock pin (0 or 1). + :param int phase: The edge of the clock that data is captured. First (0) or second (1). + Rising or falling depends on SCK clock polarity. + :param int extra_clocks: The minimum number of clock cycles to cycle the bus after CS is high. + (Used for SD cards.) + + .. note:: This class is **NOT** built into CircuitPython. See + :ref:`here for install instructions `. + + Example: + + .. code-block:: python + + import busio + import digitalio + from board import * + from adafruit_bus_device.spi_device import SPIDevice + + with busio.SPI(SCK, MOSI, MISO) as spi_bus: + cs = digitalio.DigitalInOut(D10) + device = SPIDevice(spi_bus, cs) + bytes_read = bytearray(4) + # The object assigned to spi in the with statements below + # is the original spi_bus object. We are using the busio.SPI + # operations busio.SPI.readinto() and busio.SPI.write(). + with device as spi: + spi.readinto(bytes_read) + # A second transaction + with device as spi: + spi.write(bytes_read) + """ + + def __init__( + self, + spi: SPI, + chip_select: Optional[DigitalInOut] = None, + *, + cs_active_value: bool = False, + baudrate: int = 100000, + polarity: int = 0, + phase: int = 0, + extra_clocks: int = 0 + ) -> None: + self.spi = spi + self.baudrate = baudrate + self.polarity = polarity + self.phase = phase + self.extra_clocks = extra_clocks + self.chip_select = chip_select + self.cs_active_value = cs_active_value + if self.chip_select: + self.chip_select.switch_to_output(value=not self.cs_active_value) + + def __enter__(self) -> SPI: + while not self.spi.try_lock(): + time.sleep(0) + self.spi.configure( + baudrate=self.baudrate, polarity=self.polarity, phase=self.phase + ) + if self.chip_select: + self.chip_select.value = self.cs_active_value + return self.spi + + def __exit__( + self, + exc_type: Optional[Type[type]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: + if self.chip_select: + self.chip_select.value = not self.cs_active_value + if self.extra_clocks > 0: + buf = bytearray(1) + buf[0] = 0xFF + clocks = self.extra_clocks // 8 + if self.extra_clocks % 8 != 0: + clocks += 1 + for _ in range(clocks): + self.spi.write(buf) + self.spi.unlock() + return False diff --git a/scripts/bme60.py b/scripts/bme60.py new file mode 100644 index 0000000000000000000000000000000000000000..bd2757eef6570f1ec3a9710503e699615470126a --- /dev/null +++ b/scripts/bme60.py @@ -0,0 +1,421 @@ +# The MIT License (MIT) +# +# Copyright (c) 2017 ladyada for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# We have a lot of attributes for this complex sensor. +# pylint: disable=too-many-instance-attributes + +""" +`bme680` - BME680 - Temperature, Humidity, Pressure & Gas Sensor +================================================================ + +MicroPython driver from BME680 air quality sensor, based on Adafruit_bme680 + +* Author(s): Limor 'Ladyada' Fried of Adafruit + Jeff Raber (SPI support) + and many more contributors +""" + +import time +import math +from micropython import const +from ubinascii import hexlify as hex +try: + import struct +except ImportError: + import ustruct as struct + +# I2C ADDRESS/BITS/SETTINGS +# ----------------------------------------------------------------------- +_BME680_CHIPID = const(0x61) + +_BME680_REG_CHIPID = const(0xD0) +_BME680_BME680_COEFF_ADDR1 = const(0x89) +_BME680_BME680_COEFF_ADDR2 = const(0xE1) +_BME680_BME680_RES_HEAT_0 = const(0x5A) +_BME680_BME680_GAS_WAIT_0 = const(0x64) + +_BME680_REG_SOFTRESET = const(0xE0) +_BME680_REG_CTRL_GAS = const(0x71) +_BME680_REG_CTRL_HUM = const(0x72) +_BME280_REG_STATUS = const(0xF3) +_BME680_REG_CTRL_MEAS = const(0x74) +_BME680_REG_CONFIG = const(0x75) + +_BME680_REG_PAGE_SELECT = const(0x73) +_BME680_REG_MEAS_STATUS = const(0x1D) +_BME680_REG_PDATA = const(0x1F) +_BME680_REG_TDATA = const(0x22) +_BME680_REG_HDATA = const(0x25) + +_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16) +_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127) + +_BME680_RUNGAS = const(0x10) + +_LOOKUP_TABLE_1 = (2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, + 2126008810.0, 2147483647.0, 2130303777.0, 2147483647.0, 2147483647.0, + 2143188679.0, 2136746228.0, 2147483647.0, 2126008810.0, 2147483647.0, + 2147483647.0) + +_LOOKUP_TABLE_2 = (4096000000.0, 2048000000.0, 1024000000.0, 512000000.0, 255744255.0, 127110228.0, + 64000000.0, 32258064.0, 16016016.0, 8000000.0, 4000000.0, 2000000.0, 1000000.0, + 500000.0, 250000.0, 125000.0) + + +def _read24(arr): + """Parse an unsigned 24-bit value as a floating point and return it.""" + ret = 0.0 + #print([hex(i) for i in arr]) + for b in arr: + ret *= 256.0 + ret += float(b & 0xFF) + return ret + + +class Adafruit_BME680: + """Driver from BME680 air quality sensor + + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading.""" + def __init__(self, *, refresh_rate=10): + """Check the BME680 was found, read the coefficients and enable the sensor for continuous + reads.""" + self._write(_BME680_REG_SOFTRESET, [0xB6]) + time.sleep(0.005) + + # Check device ID. + chip_id = self._read_byte(_BME680_REG_CHIPID) + if chip_id != _BME680_CHIPID: + raise RuntimeError('Failed to find BME680! Chip ID 0x%x' % chip_id) + + self._read_calibration() + + # set up heater + self._write(_BME680_BME680_RES_HEAT_0, [0x73]) + self._write(_BME680_BME680_GAS_WAIT_0, [0x65]) + + self.sea_level_pressure = 1013.25 + """Pressure in hectoPascals at sea level. Used to calibrate ``altitude``.""" + + # Default oversampling and filter register values. + self._pressure_oversample = 0b011 + self._temp_oversample = 0b100 + self._humidity_oversample = 0b010 + self._filter = 0b010 + + self._adc_pres = None + self._adc_temp = None + self._adc_hum = None + self._adc_gas = None + self._gas_range = None + self._t_fine = None + + self._last_reading = time.ticks_ms() + self._min_refresh_time = 1000 // refresh_rate + + @property + def pressure_oversample(self): + """The oversampling for pressure sensor""" + return _BME680_SAMPLERATES[self._pressure_oversample] + + @pressure_oversample.setter + def pressure_oversample(self, sample_rate): + if sample_rate in _BME680_SAMPLERATES: + self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def humidity_oversample(self): + """The oversampling for humidity sensor""" + return _BME680_SAMPLERATES[self._humidity_oversample] + + @humidity_oversample.setter + def humidity_oversample(self, sample_rate): + if sample_rate in _BME680_SAMPLERATES: + self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def temperature_oversample(self): + """The oversampling for temperature sensor""" + return _BME680_SAMPLERATES[self._temp_oversample] + + @temperature_oversample.setter + def temperature_oversample(self, sample_rate): + if sample_rate in _BME680_SAMPLERATES: + self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def filter_size(self): + """The filter size for the built in IIR filter""" + return _BME680_FILTERSIZES[self._filter] + + @filter_size.setter + def filter_size(self, size): + if size in _BME680_FILTERSIZES: + self._filter = _BME680_FILTERSIZES[size] + else: + raise RuntimeError("Invalid size") + + @property + def temperature(self): + """The compensated temperature in degrees celsius.""" + self._perform_reading() + calc_temp = (((self._t_fine * 5) + 128) / 256) + return calc_temp / 100 + + @property + def pressure(self): + """The barometric pressure in hectoPascals""" + self._perform_reading() + 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) + return calc_pres/100 + + @property + def humidity(self): + """The relative humidity in RH %""" + self._perform_reading() + 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 + + @property + def altitude(self): + """The altitude based on current ``pressure`` vs the sea level pressure + (``sea_level_pressure``) - which you must enter ahead of time)""" + pressure = self.pressure # in Si units for hPascal + return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) + + @property + def gas(self): + """The gas resistance in ohms""" + self._perform_reading() + var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536 + var2 = ((self._adc_gas * 32768) - 16777216) + var1 + var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 + calc_gas_res = (var3 + (var2 / 2)) / var2 + return int(calc_gas_res) + + def _perform_reading(self): + """Perform a single-shot reading from the sensor and fill internal data structure for + calculations""" + expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1) + if 0 <= expired < self._min_refresh_time: + time.sleep_ms(self._min_refresh_time - expired) + + # set filter + self._write(_BME680_REG_CONFIG, [self._filter << 2]) + # turn on temp oversample & pressure oversample + self._write(_BME680_REG_CTRL_MEAS, + [(self._temp_oversample << 5)|(self._pressure_oversample << 2)]) + # turn on humidity oversample + self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) + # gas measurements enabled + self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS]) + + ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) + ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! + self._write(_BME680_REG_CTRL_MEAS, [ctrl]) + new_data = False + while not new_data: + data = self._read(_BME680_REG_MEAS_STATUS, 15) + new_data = data[0] & 0x80 != 0 + time.sleep(0.005) + self._last_reading = time.ticks_ms() + + self._adc_pres = _read24(data[2:5]) / 16 + self._adc_temp = _read24(data[5:8]) / 16 + self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0] + self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64) + self._gas_range = data[14] & 0x0F + + 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) + + def _read_calibration(self): + """Read & save the calibration coefficients""" + coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) + coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) + + coeff = list(struct.unpack(' 100: + calc_hum = 100 + if calc_hum < 0: + calc_hum = 0 + return calc_hum + + @property + def altitude(self): + """The altitude based on current ``pressure`` vs the sea level pressure + (``sea_level_pressure``) - which you must enter ahead of time)""" + pressure = self.pressure # in Si units for hPascal + return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) + + @property + def gas(self): + """The gas resistance in ohms""" + self._perform_reading() + var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536 + var2 = ((self._adc_gas * 32768) - 16777216) + var1 + var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 + calc_gas_res = (var3 + (var2 / 2)) / var2 + return int(calc_gas_res) + + def _perform_reading(self): + """Perform a single-shot reading from the sensor and fill internal data structure for + calculations""" + expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1) + if 0 <= expired < self._min_refresh_time: + time.sleep_ms(self._min_refresh_time - expired) + + # set filter + self._write(_BME680_REG_CONFIG, [self._filter << 2]) + # turn on temp oversample & pressure oversample + self._write(_BME680_REG_CTRL_MEAS, + [(self._temp_oversample << 5)|(self._pressure_oversample << 2)]) + # turn on humidity oversample + self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) + # gas measurements enabled + self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS]) + + ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) + ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! + self._write(_BME680_REG_CTRL_MEAS, [ctrl]) + new_data = False + while not new_data: + data = self._read(_BME680_REG_MEAS_STATUS, 15) + new_data = data[0] & 0x80 != 0 + time.sleep(0.005) + self._last_reading = time.ticks_ms() + + self._adc_pres = _read24(data[2:5]) / 16 + self._adc_temp = _read24(data[5:8]) / 16 + self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0] + self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64) + self._gas_range = data[14] & 0x0F + + 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) + + def _read_calibration(self): + """Read & save the calibration coefficients""" + coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) + coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) + + coeff = list(struct.unpack('> 1) - 64000 + var2 = ((var1 >> 2) * (var1 >> 2 )) >> 11 + var2 = (var2 * self._pressure_calibration[5]) >> 2 + var2 = var2 + ((var1 * self._pressure_calibration[4]) << 1) + var2 = (var2 >> 2) + (self._pressure_calibration[3] << 16) + var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) * + (self._pressure_calibration[2] << 5) >> 3) + + ((self._pressure_calibration[1] * var1) >> 1)) + var1 = var1 >> 18 + var1 = ((32768 + var1) * self._pressure_calibration[0]) >> 15 + calc_pres = 1048576 - int(self._adc_pres) + calc_pres = (calc_pres - (var2 >> 12)) * 3125 + calc_pres = (calc_pres << 1) // var1 + var1 = (self._pressure_calibration[8] * (((calc_pres >> 3) * (calc_pres >> 3)) >> 13)) >> 12 + var2 = ((calc_pres >> 2) * self._pressure_calibration[7]) >> 13 + var3 = (((calc_pres >> 8) * (calc_pres >> 8) * (calc_pres >> 8)) * self._pressure_calibration[9]) >> 17 + calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] << 7)) >> 4) + return calc_pres / 100 + + @property + def humidity(self): + """The relative humidity in RH %""" + self._perform_reading() + + temp_scaled = ((self._t_fine * 5) + 128) >> 8 + var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) - + (((temp_scaled * self._humidity_calibration[2]) // 100) >> 1)) + var2 = (self._humidity_calibration[1] * + (((temp_scaled * self._humidity_calibration[3]) // 100) + + (((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) + // 100)) >> 6) // 100) + 16384)) >> 10 + var3 = var1 * var2 + var4 = self._humidity_calibration[5] << 7 + var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) // 100)) >> 4 + var5 = ((var3 >> 14) * (var3 >> 14)) >> 10 + var6 = (var4 * var5) >> 1 + calc_hum = ((var3 + var6) >> 10) / 4096 + + if calc_hum > 10000: + calc_hum = 10000 + if calc_hum < 0: + calc_hum = 0 + return calc_hum + + @property + def altitude(self): + """The altitude based on current ``pressure`` vs the sea level pressure + (``sea_level_pressure``) - which you must enter ahead of time)""" + pressure = self.pressure # in Si units for hPascal + return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) + + @property + def gas(self): + """The gas resistance in ohms""" + self._perform_reading() + var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) >> 16 + var2 = ((self._adc_gas << 15) - 16777216) + var1 + var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) >> 9 + calc_gas_res = (var3 + (var2 >> 1)) // var2 + return int(calc_gas_res) + + def _perform_reading(self): + """Perform a single-shot reading from the sensor and fill internal data structure for + calculations""" + expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1) + if 0 <= expired < self._min_refresh_time: + time.sleep_ms(self._min_refresh_time - expired) + + # set filter + self._write(_BME680_REG_CONFIG, [self._filter << 2]) + # turn on temp oversample & pressure oversample + self._write(_BME680_REG_CTRL_MEAS, + [(self._temp_oversample << 5)|(self._pressure_oversample << 2)]) + # turn on humidity oversample + self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) + # gas measurements enabled + self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS]) + + ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) + ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! + self._write(_BME680_REG_CTRL_MEAS, [ctrl]) + new_data = False + while not new_data: + data = self._read(_BME680_REG_MEAS_STATUS, 15) + new_data = data[0] & 0x80 != 0 + time.sleep(0.005) + self._last_reading = time.ticks_ms() + + self._adc_pres = _read24(data[2:5]) / 16 + self._adc_temp = _read24(data[5:8]) / 16 + self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0] + self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64) + self._gas_range = data[14] & 0x0F + + var1 = (int(self._adc_temp) >> 3) - (self._temp_calibration[0] << 1) + var2 = (var1 * self._temp_calibration[1]) >> 11 + var3 = ((var1 >> 1) * (var1 >> 1)) >> 12 + var3 = (var3 * self._temp_calibration[2] << 4) >> 14 + self._t_fine = int(var2 + var3) + + def _read_calibration(self): + """Read & save the calibration coefficients""" + coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) + coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) + + coeff = list(struct.unpack('>= 4 + + self._heat_range = (self._read_byte(0x02) & 0x30) >> 4 + self._heat_val = self._read_byte(0x00) + self._sw_err = (self._read_byte(0x04) & 0xF0) >> 4 + + def _read_byte(self, register): + """Read a byte register value and return it""" + return self._read(register, 1)[0] + + def _read(self, register, length): + raise NotImplementedError() + + def _write(self, register, values): + raise NotImplementedError() + +class BME680_I2C(Adafruit_BME680): + """Driver for I2C connected BME680. + + :param i2c: I2C device object + :param int address: I2C device address + :param bool debug: Print debug statements when True. + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading.""" + def __init__(self, i2c, address=0x77, debug=False, *, refresh_rate=10): + """Initialize the I2C device at the 'address' given""" + self._i2c = i2c + self._address = address + self._debug = debug + super().__init__(refresh_rate=refresh_rate) + + def _read(self, register, length): + """Returns an array of 'length' bytes from the 'register'""" + result = bytearray(length) + self._i2c.readfrom_mem_into(self._address, register & 0xff, result) + if self._debug: + print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result])) + return result + + def _write(self, register, values): + """Writes an array of 'length' bytes to the 'register'""" + if self._debug: + print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values])) + for value in values: + self._i2c.writeto_mem(self._address, register, bytearray([value & 0xFF])) + register += 1 + + +class BME680_SPI(Adafruit_BME680): + """Driver for SPI connected BME680. + + :param spi: SPI device object, configured + :param cs: Chip Select Pin object, configured to OUT mode + :param bool debug: Print debug statements when True. + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading. + """ + + def __init__(self, spi, cs, debug=False, *, refresh_rate=10): + self._spi = spi + self._cs = cs + self._debug = debug + self._cs(1) + super().__init__(refresh_rate=refresh_rate) + + def _read(self, register, length): + if register != _BME680_REG_PAGE_SELECT: + # _BME680_REG_PAGE_SELECT exists in both SPI memory pages + # For all other registers, we must set the correct memory page + self._set_spi_mem_page(register) + register = (register | 0x80) & 0xFF # Read single, bit 7 high. + + try: + self._cs(0) + self._spi.write(bytearray([register])) # pylint: disable=no-member + result = bytearray(length) + self._spi.readinto(result) # pylint: disable=no-member + if self._debug: + print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result])) + except Exception as e: + print (e) + result = None + finally: + self._cs(1) + return result + + def _write(self, register, values): + if register != _BME680_REG_PAGE_SELECT: + # _BME680_REG_PAGE_SELECT exists in both SPI memory pages + # For all other registers, we must set the correct memory page + self._set_spi_mem_page(register) + register &= 0x7F # Write, bit 7 low. + try: + self._cs(0) + buffer = bytearray(2 * len(values)) + for i, value in enumerate(values): + buffer[2 * i] = register + i + buffer[2 * i + 1] = value & 0xFF + self._spi.write(buffer) # pylint: disable=no-member + if self._debug: + print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values])) + except Exception as e: + print (e) + finally: + self._cs(1) + + def _set_spi_mem_page(self, register): + spi_mem_page = 0x00 + if register < 0x80: + spi_mem_page = 0x10 + self._write(_BME680_REG_PAGE_SELECT, [spi_mem_page]) diff --git a/scripts/constants.py b/scripts/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..2930feb75d6e174f6cace8be6ea46a64fc2eb94d --- /dev/null +++ b/scripts/constants.py @@ -0,0 +1,12 @@ +# constants.py + +# Wi-Fi Configuration +WIFI_SSID = 'Pixel 8' +WIFI_PASSWORD = '123456789' + +# MQTT Configuration +MQTT_BROKER = 'b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud' +MQTT_PORT = 8883 +MQTT_TOPIC = 'sensors/bme680/data' +MQTT_USER = 'LuthiraMQ' +MQTT_PASSWORD = 'Password1118.' diff --git a/scripts/demo.py b/scripts/demo.py new file mode 100644 index 0000000000000000000000000000000000000000..ad6903b805689d90b1d875588e207c969789daec --- /dev/null +++ b/scripts/demo.py @@ -0,0 +1,18 @@ +from bme680 import * +from machine import I2C, Pin +import time + +i2c = I2C(0, scl=Pin(5), sda=Pin(4)) + +bme = BME680_I2C(i2c) + +while True: + print("--------------------------------------------------") + print() + print("Temperature: {:.2f} °C".format(bme.temperature)) + print("Humidity: {:.2f} %".format(bme.humidity)) + print("Pressure: {:.2f} hPa".format(bme.pressure)) + print("Gas: {:.2f} ohms".format(bme.gas)) + print() + time.sleep(3) + diff --git a/scripts/hivemq-com-chain.der b/scripts/hivemq-com-chain.der new file mode 100644 index 0000000000000000000000000000000000000000..ac22dcf9e8b888a98c10bd12f2108feb696b68c8 Binary files /dev/null and b/scripts/hivemq-com-chain.der differ diff --git a/scripts/lib/CHANGELOG.md b/scripts/lib/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..78fec497a1305d089d4491f865400371539ec075 --- /dev/null +++ b/scripts/lib/CHANGELOG.md @@ -0,0 +1,50 @@ +2.0.0 +----- + +* Repackage to hatch/pyproject.toml +* Drop Python 2.7 support +* Switch from smbu2 to smbus2 + +1.1.1 +----- + +* New: constants to clarify heater on/off states + +1.1.0 +----- + +* New: support for BME688 "high" gas resistance variant +* New: set/get gas heater disable bit +* Enhancement: fail with descriptive RuntimeError when chip is not detected + +1.0.5 +----- + +* New: set_temp_offset to calibrate temperature offset in degrees C + +1.0.4 +----- + +* Fix to range_sw_err for extremely high gas readings +* Convert to unsigned int to fix negative gas readings + +1.0.3 +----- + +* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3 + +1.0.2 +----- + +* Fixed set_gas_heater_temperature to avoid i2c TypeError + +1.0.1 +----- + +* Added Manifest to Python package + +1.0.0 +----- + +* Initial release + diff --git a/scripts/lib/LICENSE b/scripts/lib/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..b3e25b24fc4c3541ddb9d968fe85777e7b77a07d --- /dev/null +++ b/scripts/lib/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Pimoroni Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/lib/README.md b/scripts/lib/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0c71b7f75523c9101bde1f59b8318ab0b8094e69 --- /dev/null +++ b/scripts/lib/README.md @@ -0,0 +1,56 @@ +# BME680 + +[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme680-python/test.yml?branch=main)](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme680-python?branch=main) +[![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680) +[![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](https://pypi.python.org/pypi/bme680) + +https://shop.pimoroni.com/products/bme680 + +The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality. + +## Installing + +### Full install (recommended): + +We've created an easy installation script that will install all pre-requisites and get your BME680 +up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal +on your Raspberry Pi desktop, as illustrated below: + +![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) + +In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh +``` + +**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: + +``` +source ~/.virtualenvs/pimoroni/bin/activate +``` + +### Development: + +If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh --unstable +``` + +In all cases you will have to enable the i2c bus: + +``` +sudo raspi-config nonint do_i2c 0 +``` + +## Documentation & Support + +* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout +* Get help - http://forums.pimoroni.com/c/support + diff --git a/scripts/lib/adafruit_blinka-8.49.0.dist-info/METADATA b/scripts/lib/adafruit_blinka-8.49.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..8854faa0ce9f7d54ab5024bba32fb0e6744aa632 --- /dev/null +++ b/scripts/lib/adafruit_blinka-8.49.0.dist-info/METADATA @@ -0,0 +1,8 @@ +Metadata-Version: 2.1 +Name: adafruit-blinka +Version: 8.49.0 +Summary: Dummy package for satisfying formal requirements +Home-page: ? +Author: ? + +? diff --git a/scripts/lib/adafruit_blinka-8.49.0.dist-info/RECORD b/scripts/lib/adafruit_blinka-8.49.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..a105ee1c59324bb3add7655194199ec7e90b5575 --- /dev/null +++ b/scripts/lib/adafruit_blinka-8.49.0.dist-info/RECORD @@ -0,0 +1,2 @@ +adafruit_blinka-8.49.0.dist-info/METADATA,, +adafruit_blinka-8.49.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/lib/as7341.py b/scripts/lib/as7341.py new file mode 100644 index 0000000000000000000000000000000000000000..26dc8f892e2799ac790227565b73169df2ee5353 --- /dev/null +++ b/scripts/lib/as7341.py @@ -0,0 +1,608 @@ +""" +This file licensed under the MIT License and incorporates work covered by +the following copyright and permission notice: + +The MIT License (MIT) + +Copyright (c) 2022-2022 Rob Hamerling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Rob Hamerling, Version 0.0, August 2022 + + Original by WaveShare for Raspberry Pi, part of: + https://www.waveshare.com/w/upload/b/b3/AS7341_Spectral_Color_Sensor_code.7z + + Converted to Micropython for use with MicroPython devices such as ESP32 + - pythonized (in stead of 'literal' translation of C code) + - instance of AS7341 requires specification of I2C interface + - added I2C read/write error detection + - added check for connected AS7341 incl. device ID + - some code optimization (esp. adding I2C word/block reads/writes) + - Replaced bit addressing like (1<<5) by symbolic name with bit mask + - moved SMUX settings for predefined channel mappings to a dictionary + and as a separate file to allow changes or additional configurations + by the user without changing the driver + - several changes of names of functions and constants + (incl. camel case -> word separators with underscores) + - added comments, doc-strings with explanation and/or argumentation + - several other improvements and some corrections + + Remarks: + - Automatic Gain Control (AGC) is not supported + - No provisions for SYND mode + +""" + +from time import sleep_ms + +from as7341_smux_select import * # predefined SMUX configurations + +AS7341_I2C_ADDRESS = const(0x39) # I2C address of AS7341 +AS7341_ID_VALUE = const(0x24) # AS7341 Part Number Identification +# (excl 2 low order bits) + +# Symbolic names for registers and some selected bit fields +# Note: ASTATUS, ITIME and CHx_DATA in address range 0x60--0x6F are not used +AS7341_CONFIG = const(0x70) +AS7341_CONFIG_INT_MODE_SPM = const(0x00) +AS7341_MODE_SPM = AS7341_CONFIG_INT_MODE_SPM # alias +AS7341_CONFIG_INT_MODE_SYNS = const(0x01) +AS7341_MODE_SYNS = AS7341_CONFIG_INT_MODE_SYNS # alias +AS7341_CONFIG_INT_MODE_SYND = const(0x03) +AS7341_MODE_SYND = AS7341_CONFIG_INT_MODE_SYND # alias +AS7341_CONFIG_INT_SEL = const(0x04) +AS7341_CONFIG_LED_SEL = const(0x08) +AS7341_STAT = const(0x71) +AS7341_STAT_READY = const(0x01) +AS7341_STAT_WAIT_SYNC = const(0x02) +AS7341_EDGE = const(0x72) +AS7341_GPIO = const(0x73) +AS7341_GPIO_PD_INT = const(0x01) +AS7341_GPIO_PD_GPIO = const(0x02) +AS7341_LED = const(0x74) +AS7341_LED_LED_ACT = const(0x80) +AS7341_ENABLE = const(0x80) +AS7341_ENABLE_PON = const(0x01) +AS7341_ENABLE_SP_EN = const(0x02) +AS7341_ENABLE_WEN = const(0x08) +AS7341_ENABLE_SMUXEN = const(0x10) +AS7341_ENABLE_FDEN = const(0x40) +AS7341_ATIME = const(0x81) +AS7341_WTIME = const(0x83) +AS7341_SP_TH_LOW = const(0x84) +AS7341_SP_TH_L_LSB = const(0x84) +AS7341_SP_TH_L_MSB = const(0x85) +AS7341_SP_TH_HIGH = const(0x86) +AS7341_SP_TH_H_LSB = const(0x86) +AS7341_SP_TH_H_MSB = const(0x87) +AS7341_AUXID = const(0x90) +AS7341_REVID = const(0x91) +AS7341_ID = const(0x92) +AS7341_STATUS = const(0x93) +AS7341_STATUS_ASAT = const(0x80) +AS7341_STATUS_AINT = const(0x08) +AS7341_STATUS_FINT = const(0x04) +AS7341_STATUS_C_INT = const(0x02) +AS7341_STATUS_SINT = const(0x01) +AS7341_ASTATUS = const(0x94) # start of bulk read (incl 6 counts) +AS7341_ASTATUS_ASAT_STATUS = const(0x80) +AS7341_ASTATUS_AGAIN_STATUS = const(0x0F) +AS7341_CH_DATA = const(0x95) # start of the 6 channel counts +AS7341_CH0_DATA_L = const(0x95) +AS7341_CH0_DATA_H = const(0x96) +AS7341_CH1_DATA_L = const(0x97) +AS7341_CH1_DATA_H = const(0x98) +AS7341_CH2_DATA_L = const(0x99) +AS7341_CH2_DATA_H = const(0x9A) +AS7341_CH3_DATA_L = const(0x9B) +AS7341_CH3_DATA_H = const(0x9C) +AS7341_CH4_DATA_L = const(0x9D) +AS7341_CH4_DATA_H = const(0x9E) +AS7341_CH5_DATA_L = const(0x9F) +AS7341_CH5_DATA_H = const(0xA0) +AS7341_STATUS_2 = const(0xA3) +AS7341_STATUS_2_AVALID = const(0x40) +AS7341_STATUS_3 = const(0xA4) +AS7341_STATUS_5 = const(0xA6) +AS7341_STATUS_6 = const(0xA7) +AS7341_CFG_0 = const(0xA9) +AS7341_CFG_0_WLONG = const(0x04) +AS7341_CFG_0_REG_BANK = const(0x10) # datasheet fig 82 (! fig 32) +AS7341_CFG_0_LOW_POWER = const(0x20) +AS7341_CFG_1 = const(0xAA) +AS7341_CFG_3 = const(0xAC) +AS7341_CFG_6 = const(0xAF) +AS7341_CFG_6_SMUX_CMD_ROM = const(0x00) +AS7341_CFG_6_SMUX_CMD_READ = const(0x08) +AS7341_CFG_6_SMUX_CMD_WRITE = const(0x10) +AS7341_CFG_8 = const(0xB1) +AS7341_CFG_9 = const(0xB2) +AS7341_CFG_10 = const(0xB3) +AS7341_CFG_12 = const(0xB5) +AS7341_PERS = const(0xBD) +AS7341_GPIO_2 = const(0xBE) +AS7341_GPIO_2_GPIO_IN = const(0x01) +AS7341_GPIO_2_GPIO_OUT = const(0x02) +AS7341_GPIO_2_GPIO_IN_EN = const(0x04) +AS7341_GPIO_2_GPIO_INV = const(0x08) +AS7341_ASTEP = const(0xCA) +AS7341_ASTEP_L = const(0xCA) +AS7341_ASTEP_H = const(0xCB) +AS7341_AGC_GAIN_MAX = const(0xCF) +AS7341_AZ_CONFIG = const(0xD6) +AS7341_FD_TIME_1 = const(0xD8) +AS7341_FD_TIME_2 = const(0xDA) +AS7341_FD_CFG0 = const(0xD7) +AS7341_FD_STATUS = const(0xDB) +AS7341_FD_STATUS_FD_100HZ = const(0x01) +AS7341_FD_STATUS_FD_120HZ = const(0x02) +AS7341_FD_STATUS_FD_100_VALID = const(0x04) +AS7341_FD_STATUS_FD_120_VALID = const(0x08) +AS7341_FD_STATUS_FD_SAT_DETECT = const(0x10) +AS7341_FD_STATUS_FD_MEAS_VALID = const(0x20) +AS7341_INTENAB = const(0xF9) +AS7341_INTENAB_SP_IEN = const(0x08) +AS7341_CONTROL = const(0xFA) +AS7341_FIFO_MAP = const(0xFC) +AS7341_FIFO_LVL = const(0xFD) +AS7341_FDATA = const(0xFE) +AS7341_FDATA_L = const(0xFE) +AS7341_FDATA_H = const(0xFF) + + +class AS7341: + """Class for AS7341: 11 Channel Multi-Spectral Digital Sensor""" + + def __init__(self, i2c, addr=AS7341_I2C_ADDRESS): + """specification of active I2C object is mandatory + specification of I2C address of AS7341 is optional + """ + self.__bus = i2c + self.__address = addr + self.__buffer1 = bytearray(1) # I2C I/O buffer for byte + self.__buffer2 = bytearray(2) # I2C I/O buffer for word + self.__buffer13 = bytearray(13) # I2C I/O buffer ASTATUS + 6 counts + self.__measuremode = AS7341_MODE_SPM # default measurement mode + self.__connected = self.reset() # recycle power, check AS7341 presence + + """ --------- 'private' functions ----------- """ + + def __read_byte(self, reg): + """read byte, return byte (integer) value""" + try: + self.__bus.readfrom_mem_into(self.__address, reg, self.__buffer1) + return self.__buffer1[0] # return integer value + except Exception as err: + print("I2C read_byte at 0x{:02X}, error".format(reg), err) + return -1 # indication 'no receive' + + def __read_word(self, reg): + """read 2 consecutive bytes, return integer value (little Endian)""" + try: + self.__bus.readfrom_mem_into(self.__address, reg, self.__buffer2) + return int.from_bytes(self.__buffer2, "little") # return word value + except Exception as err: + print("I2C read_word at 0x{:02X}, error".format(reg), err) + return -1 # indication 'no receive' + + def __read_all_channels(self): + """read ASTATUS register and all channels, return list of 6 integer values""" + try: + self.__bus.readfrom_mem_into( + self.__address, AS7341_ASTATUS, self.__buffer13 + ) + return [ + int.from_bytes(self.__buffer13[1 + 2 * i : 3 + 2 * i], "little") + for i in range(6) + ] + except Exception as err: + print( + "I2C read_all_channels at 0x{:02X}, error".format(AS7341_ASTATUS), err + ) + return [] # empty list + + def __write_byte(self, reg, value): + """write a single byte to the specified register""" + self.__buffer1[0] = value & 0xFF + try: + self.__bus.writeto_mem(self.__address, reg, self.__buffer1) + sleep_ms(10) + except Exception as err: + print("I2C write_byte at 0x{:02X}, error".format(reg), err) + return False + return True + + def __write_word(self, reg, value): + """write a word as 2 bytes (little endian encoding) + to adresses + 0 and + 1 + """ + self.__buffer2[0] = value & 0xFF # low byte + self.__buffer2[1] = (value >> 8) & 0xFF # high byte + try: + self.__bus.writeto_mem(self.__address, reg, self.__buffer2) + sleep_ms(20) + except Exception as err: + print("I2C write_word at 0x{:02X}, error".format(reg), err) + return False + return True + + def __write_burst(self, reg, value): + """write an array of bytes to consucutive addresses starting """ + try: + self.__bus.writeto_mem(self.__address, reg, value) + sleep_ms(100) + except Exception as err: + print("I2C write_burst at 0x{:02X}, error".format(reg), err) + return False + return True + + def __modify_reg(self, reg, mask, flag=True): + """modify register with + True means 'or' with : set the bit(s) + False means 'and' with inverted : reset the bit(s) + Notes: 1. Works only with '1' bits in + (in most cases contains a single 1-bit!) + 2. When is in region 0x60-0x74 + bank 1 is supposed be set by caller + """ + data = self.__read_byte(reg) # read + if flag: + data |= mask + else: + data &= ~mask + self.__write_byte(reg, data) # rewrite + + def __set_bank(self, bank=1): + """select registerbank + 1 for access to regs 0x60-0x74 + 0 for access to regs 0x80-0xFF + Note: It seems that reg CFG_0 (0x93) is accessible + even when REG_BANK bit is set for 0x60-0x74, + otherwise it wouldn't be possible to reset REG_BANK + Datasheet isn't clear about this. + """ + if bank in (0, 1): + self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_REG_BANK, bank == 1) + + """ ----------- 'public' functions ----------- """ + + def enable(self): + """enable device (only power on)""" + self.__write_byte(AS7341_ENABLE, AS7341_ENABLE_PON) + + def disable(self): + """disable all functions and power off""" + self.__set_bank(1) # CONFIG register is in bank 1 + self.__write_byte(AS7341_CONFIG, 0x00) # INT, LED off, SPM mode + self.__set_bank(0) + self.__write_byte(AS7341_ENABLE, 0x00) # power off + + def isconnected(self): + """determine if AS7341 is successfully initialized (True/False)""" + return self.__connected + + def reset(self): + """Cycle power and check if AS7341 is (re-)connected + When connected set (restore) measurement mode + """ + self.disable() # power-off ('reset') + sleep_ms(50) # quisce + self.enable() # (only) power-on + sleep_ms(50) # settle + id = self.__read_byte(AS7341_ID) # obtain Part Number ID + if id < 0: # read error + print( + "Failed to contact AS7341 at I2C address 0x{:02X}".format( + self.__address + ) + ) + return False + else: + if not (id & (~0x03)) == AS7341_ID_VALUE: # ID in bits 7..2 bits + print( + "No AS7341: ID = 0x{:02X}, expected 0x{:02X}".format( + id, AS7341_ID_VALUE + ) + ) + return False + self.set_measure_mode(self.__measuremode) # configure chip + return True + + def measurement_completed(self): + """check if measurement completed (return True) or otherwise return False""" + return bool(self.__read_byte(AS7341_STATUS_2) & AS7341_STATUS_2_AVALID) + + def set_spectral_measurement(self, flag=True): + """enable (flag == True) spectral measurement or otherwise disable it""" + self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_SP_EN, flag) + + def set_smux(self, flag=True): + """enable (flag == True) SMUX or otherwise disable it""" + self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_SMUXEN, flag) + + def set_measure_mode(self, mode=AS7341_CONFIG_INT_MODE_SPM): + """configure the AS7341 for a specific measurement mode + when interrupt needed it must be configured separately + """ + if mode in ( + AS7341_CONFIG_INT_MODE_SPM, # meas. started by SP_EN + AS7341_CONFIG_INT_MODE_SYNS, # meas. started by GPIO + AS7341_CONFIG_INT_MODE_SYND, + ): # meas. started by GPIO + EDGE + self.__measuremode = mode # store new measurement mode + self.__set_bank(1) # CONFIG register is in bank 1 + data = self.__read_byte(AS7341_CONFIG) & (~3) # discard 2 LSbs (mode) + data |= mode # insert new mode + self.__write_byte(AS7341_CONFIG, data) # modify measurement mode + self.__set_bank(0) + + def channel_select(self, selection): + """select one from a series of predefined SMUX configurations + should be a key in dictionary AS7341_SMUX_SELECT + 20 bytes of memory starting from address 0 will be overwritten. + """ + if selection in AS7341_SMUX_SELECT: + self.__write_burst(0x00, AS7341_SMUX_SELECT[selection]) + else: + print(selection, "is unknown in AS7341_SMUX_SELECT") + + def start_measure(self, selection): + """select SMUX configuration, prepare and start measurement""" + self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_LOW_POWER, False) # no low power + self.set_spectral_measurement(False) # quiesce + self.__write_byte(AS7341_CFG_6, AS7341_CFG_6_SMUX_CMD_WRITE) # write mode + if self.__measuremode == AS7341_CONFIG_INT_MODE_SPM: + self.channel_select(selection) + self.set_smux(True) + elif self.__measuremode == AS7341_CONFIG_INT_MODE_SYNS: + self.channel_select(selection) + self.set_smux(True) + self.set_gpio_mode(AS7341_GPIO_2_GPIO_IN_EN) + self.set_spectral_measurement(True) + if self.__measuremode == AS7341_CONFIG_INT_MODE_SPM: + while not self.measurement_completed(): + sleep_ms(50) + + def get_channel_data(self, channel): + """read count of a single channel (channel in range 0..5) + with or without measurement, just read count of one channel + contents depend on previous selection with 'start_measure' + auto-zero feature may result in value 0! + """ + data = 0 # default + if 0 <= channel <= 5: + data = self.__read_word(AS7341_CH_DATA + channel * 2) + return data # return integer value + + def get_spectral_data(self): + """obtain counts of all channels + return a tuple of 6 counts (integers) of the channels + contents depend on previous selection with 'start_measure' + """ + return self.__read_all_channels() # return a tuple! + + def set_flicker_detection(self, flag=True): + """enable (flag == True) flicker detection or otherwise disable it""" + self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_FDEN, flag) + + def get_flicker_frequency(self): + """Determine flicker frequency in Hz. Returns 100, 120 or 0 + Integration time and gain for flicker detection is the same as for + other channels, the dedicated FD_TIME and FD_GAIN are not supported + """ + self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_LOW_POWER, False) # no low power + self.set_spectral_measurement(False) + self.__write_byte(AS7341_CFG_6, AS7341_CFG_6_SMUX_CMD_WRITE) + self.channel_select("FD") # select flicker detection only + self.set_smux(True) + self.set_spectral_measurement(True) + self.set_flicker_detection(True) + for _ in range(10): # limited wait for completion + fd_status = self.__read_byte(AS7341_FD_STATUS) + if fd_status & AS7341_FD_STATUS_FD_MEAS_VALID: + break + # print("Flicker measurement not completed") + sleep_ms(100) + else: # timeout + print("Flicker measurement timed out") + return 0 + for _ in range(10): # limited wait for calculation + fd_status = self.__read_byte(AS7341_FD_STATUS) + if (fd_status & AS7341_FD_STATUS_FD_100_VALID) or ( + fd_status & AS7341_FD_STATUS_FD_120_VALID + ): + break + # print("Flicker calculation not completed") + sleep_ms(100) + else: # timeout + print("Flicker frequency calculation timed out") + return 0 + # print("FD_STATUS", "0x{:02X}".format(fd_status)) + self.set_flicker_detection(False) # disable + self.__write_byte(AS7341_FD_STATUS, 0x3C) # clear all FD STATUS bits + if (fd_status & AS7341_FD_STATUS_FD_100_VALID) and ( + fd_status & AS7341_FD_STATUS_FD_100HZ + ): + return 100 + elif (fd_status & AS7341_FD_STATUS_FD_120_VALID) and ( + fd_status & AS7341_FD_STATUS_FD_120HZ + ): + return 120 + return 0 + + def set_gpio_mode(self, mode): + """Configure mode of GPIO pin. + Allow only input-enable or output (with or without inverted) + specify 0x00 to reset the mode of the GPIO pin. + Notes: 1. It seems that GPIO_INV bit must be set + together with GPIO_IN_EN. + Proof: Use a pull-up resistor between GPIO and 3.3V: + - when program is ot started GPIO is high + - when program is started (GPIO_IN_EN=1) GPIO becomes low + - when also GPIO_INV=1 GPIO behaves normally + Maybe it is a quirk of the used test-board. + 2. GPIO output is not tested + (dataset lacks info how to set/reset GPIO) + """ + if mode in ( + 0x00, + AS7341_GPIO_2_GPIO_OUT, + AS7341_GPIO_2_GPIO_OUT | AS7341_GPIO_2_GPIO_INV, + AS7341_GPIO_2_GPIO_IN_EN, + AS7341_GPIO_2_GPIO_IN_EN | AS7341_GPIO_2_GPIO_INV, + ): + if mode == AS7341_GPIO_2_GPIO_IN_EN: # input mode + mode |= AS7341_GPIO_2_GPIO_INV # add 'inverted' + self.__write_byte(AS7341_GPIO_2, mode) + + def get_gpio_value(self): + """Determine GPIO value (when GPIO enabled for IN_EN) + returns 0 (low voltage) or 1 (high voltage) + """ + # print("GPIO_2 = 0x{:02X}".format(self.__read_byte(AS7341_GPIO_2))) + return self.__read_byte(AS7341_GPIO_2) & AS7341_GPIO_2_GPIO_IN + + def set_astep(self, value): + """set ASTEP size (range 0..65534 -> 2.78 usec .. 182 msec)""" + if 0 <= value <= 65534: + self.__write_word(AS7341_ASTEP, value) + + def set_atime(self, value): + """set number of integration steps (range 0..255 -> 1..256 ASTEPs)""" + self.__write_byte(AS7341_ATIME, value) + + def get_integration_time(self): + """return actual total integration time (atime * astep) + in milliseconds (valid with SPM and SYNS measurement mode) + """ + return ( + (self.__read_word(AS7341_ASTEP) + 1) + * (self.__read_byte(AS7341_ATIME) + 1) + * 2.78 + / 1000 + ) + + def set_again(self, code): + """set AGAIN (code in range 0..10 -> gain factor 0.5 .. 512) + value 0 1 2 3 4 5 6 7 8 9 10 + gain: *0.5 | *1 | *2 | *4 | *8 | *16 | *32 | *64 | *128 | *256 | *512 + """ + if 0 <= code <= 10: + self.__write_byte(AS7341_CFG_1, code) + + def get_again(self): + """obtain actual gain code (in range 0 .. 10)""" + return self.__read_byte(AS7341_CFG_1) + + def set_again_factor(self, factor): + """'inverse' of 'set_again': gain factor -> code 0 .. 10 + is rounded down to nearest power of 2 (in range 0.5 .. 512) + """ + code = 10 + gain = 512 + while gain > factor and code > 0: + gain /= 2 + code -= 1 + # print("factor", factor, "gain", gain, "code", code) + self.__write_byte(AS7341_CFG_1, code) + + def get_again_factor(self): + """obtain actual gain factor (in range 0.5 .. 512)""" + return 2 ** (self.__read_byte(AS7341_CFG_1) - 1) + + def set_wen(self, flag=True): + """enable (flag=True) or otherwise disable use of WTIME (auto re-start)""" + self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_WEN, flag) + + def set_wtime(self, wtime): + """set WTIME when auto-re-start is desired (in range 0 .. 0xFF) + 0 -> 2.78ms, 0xFF -> 711.7 ms + Note: The WEN bit in ENABLE should be set as well: set_wen() + """ + self.__write_byte(AS7341_WTIME, wtime) + + def set_led_current(self, current): + """Control current of onboard LED in milliamperes + LED-current is (here) limited to the range 4..20 mA + use only even numbers (4,6,8,... etc) + Specification outside this range results in LED OFF + """ + self.__set_bank(1) # CONFIG and LED registers in bank 1 + if 4 <= current <= 20: # within limits: 4..20 mA + self.__modify_reg(AS7341_CONFIG, AS7341_CONFIG_LED_SEL, True) + # print("Reg. CONFIG (0x70) now 0x{:02X}".format(self.__read_byte(0x70))) + data = AS7341_LED_LED_ACT + ((current - 4) // 2) # LED on with PWM + else: + self.__modify_reg(AS7341_CONFIG, AS7341_CONFIG_LED_SEL, False) + data = 0 # LED off, PWM 0 + self.__write_byte(AS7341_LED, data) + # print("reg 0x74 (LED) now 0x{:02X}".format(self.__read_byte(0x74))) + self.__set_bank(0) + sleep_ms(100) + + def check_interrupt(self): + """Check for Spectral or Flicker Detect saturation interrupt""" + data = self.__read_byte(AS7341_STATUS) + if data & AS7341_STATUS_ASAT: + print("Spectral interrupt generation!") + return True + return False + + def clear_interrupt(self): + """clear all interrupt signals""" + self.__write_byte(AS7341_STATUS, 0xFF) + + def set_spectral_interrupt(self, flag=True): + """enable (flag == True) or otherwise disable spectral interrupts""" + self.__modify_reg(AS7341_INTENAB, AS7341_INTENAB_SP_IEN, flag) + + def set_interrupt_persistence(self, value): + """configure interrupt persistance""" + if 0 <= value <= 15: + self.__write_byte(AS7341_PERS, value) + + def set_spectral_threshold_channel(self, value): + """select channel (0..4) for interrupts, persistence and AGC""" + if 0 <= value <= 4: + self.__write_byte(AS7341_CFG_12, value) + + def set_thresholds(self, lo, hi): + """Set thresholds (when lo < hi)""" + if lo < hi: + self.__write_word(AS7341_SP_TH_LOW, lo) + self.__write_word(AS7341_SP_TH_HIGH, hi) + sleep_ms(20) + + def get_thresholds(self): + """obtain and return tuple with low and high threshold values""" + lo = self.__read_word(AS7341_SP_TH_LOW) + hi = self.__read_word(AS7341_SP_TH_HIGH) + return (lo, hi) + + def set_syns_int(self): + """select SYNS mode and signal SYNS interrupt on Pin INT""" + self.__set_bank(1) # CONFIG register is in bank 1 + self.__write_byte( + AS7341_CONFIG, AS7341_CONFIG_INT_SEL | AS7341_CONFIG_INT_MODE_SYNS + ) + self.__set_bank(0) + + +# diff --git a/scripts/lib/as7341_sensor.py b/scripts/lib/as7341_sensor.py new file mode 100644 index 0000000000000000000000000000000000000000..c9ef947366f2839f7ff14fd5c774a6ff8fec53b8 --- /dev/null +++ b/scripts/lib/as7341_sensor.py @@ -0,0 +1,149 @@ +"""Sterling Baird: wrapper class for AS7341 sensor.""" + +from math import log + +from as7341 import AS7341, AS7341_MODE_SPM +from machine import I2C, Pin + + +class ExternalDeviceNotFound(OSError): + pass + + +class Sensor: + def __init__( + self, atime=100, astep=999, gain=8, i2c=I2C(1, scl=Pin(27), sda=Pin(26)) + ): + """Wrapper for Rob Hamerling's AS7341 implementation. + + Mimics the original CircuitPython class a bit more, specific to the needs of + SDL-Demo. + + Rob Hamerling's implementation: + - https://gitlab.com/robhamerling/micropython-as7341 + + Original Circuit Python repo: + - https://github.com/adafruit/Adafruit_CircuitPython_AS7341 + + Parameters + ---------- + atime : int, optional + The integration time step size in 2.78 microsecond increments, by default 100 + astep : int, optional + The integration time step count. Total integration time will be (ATIME + 1) + * (ASTEP + 1) * 2.78µS, by default 999, meaning 281 ms assuming atime=100 + gain : int, optional + The ADC gain multiplier, by default 128 + i2c : I2C, optional + The I2C bus, by default machine.I2C(1, scl=machine.Pin(27), + sda=machine.Pin(26)) + + Raises + ------ + ExternalDeviceNotFound + Couldn't connect to AS7341. + + Examples + -------- + >>> sensor = Sensor(atime=29, astep=599, again=4) + >>> channel_data = sensor.all_channels + """ + + # i2c = machine.SoftI2C(scl=Pin(27), sda=Pin(26)) + self.i2c = i2c + addrlist = " ".join(["0x{:02X}".format(x) for x in i2c.scan()]) # type: ignore + print("Detected devices at I2C-addresses:", addrlist) + + sensor = AS7341(i2c) + + if not sensor.isconnected(): + raise ExternalDeviceNotFound("Failed to contact AS7341, terminating") + + sensor.set_measure_mode(AS7341_MODE_SPM) + + sensor.set_atime(atime) + sensor.set_astep(astep) + sensor.set_again(gain) + + self.sensor = sensor + + self.__atime = atime + self.__astep = astep + self.__gain = gain + + @property + def _atime(self): + return self.__atime + + @_atime.setter + def _atime(self, value): + self.__atime = value + self.sensor.set_atime(value) + + @property + def _astep(self): + return self.__astep + + @_astep.setter + def _astep(self, value): + self.__atime = value + self.sensor.set_astep(value) + + @property + def _gain(self): + return self.__gain + + @_gain.setter + def _gain(self, gain): + """set AGAIN (code in range 0..10 -> gain factor 0.5 .. 512) + gain: *0.5 | *1 | *2 | *4 | *8 | *16 | *32 | *64 | *128 | *256 | *512 + code 0 1 2 3 4 5 6 7 8 9 10 + """ + self.__gain = gain + # gain == 0.5 * 2 ** code --> code == 1.4427 Ln[2 * gain] (via Mathematica) + code = int(round(1.4427 * log(2 * gain))) + self.sensor.set_again(code) + + @property + def all_channels(self): + self.sensor.start_measure("F1F4CN") + f1, f2, f3, f4, clr, nir = self.sensor.get_spectral_data() + + self.sensor.start_measure("F5F8CN") + f5, f6, f7, f8, clr, nir = self.sensor.get_spectral_data() + + clr, nir # to ignore "unused" linting warnings + + return [f1, f2, f3, f4, f5, f6, f7, f8] + + @property + def all_channels_clr_nir(self): + self.sensor.start_measure("F1F4CN") + f1, f2, f3, f4, clr, nir = self.sensor.get_spectral_data() + + self.sensor.start_measure("F5F8CN") + f5, f6, f7, f8, clr, nir = self.sensor.get_spectral_data() + + clr, nir # to ignore "unused" linting warnings + + return [f1, f2, f3, f4, f5, f6, f7, f8, clr, nir] + + def disable(self): + self.sensor.disable() + + +# %% Code Graveyard +# gain_to_code_lookup = { +# 0.5: 1, +# 1: 1, +# 2: 2, +# 4: 3, +# 8: 4, +# 16: 5, +# 32: 6, +# 64: 7, +# 128: 8, +# 256: 9, +# 512: 10, +# } +# code = gain_to_code_lookup[gain] diff --git a/scripts/lib/as7341_smux_select.py b/scripts/lib/as7341_smux_select.py new file mode 100644 index 0000000000000000000000000000000000000000..edea2f4047371887dceed11a32ef184cceb8d4df --- /dev/null +++ b/scripts/lib/as7341_smux_select.py @@ -0,0 +1,49 @@ +""" +This file licensed under the MIT License and incorporates work covered by +the following copyright and permission notice: + +The MIT License (MIT) + +Copyright (c) 2022-2022 Rob Hamerling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +""" Dictionary with specific SMUX configurations for AS7341 + See AMS Application Note AS7341_AN000666_1.01.pdf + for detailed instructions how to configure the channel mapping. + The Application Note can be found in one of the evaluation packages, e.g. + AS7341_EvalSW_Reflection_v1-26-3/Documents/application notes/SMUX/ + + This file should be imported by AS7341.py with: + from as7341_smux_select import * +""" +AS7341_SMUX_SELECT = { + # F1 through F4, CLEAR, NIR: + "F1F4CN": b"\x30\x01\x00\x00\x00\x42\x00\x00\x50\x00\x00\x00\x20\x04\x00\x30\x01\x50\x00\x06", + # F5 through F8, CLEAR, NIR: + "F5F8CN": b"\x00\x00\x00\x40\x02\x00\x10\x03\x50\x10\x03\x00\x00\x00\x24\x00\x00\x50\x00\x06", + # F2 through F7: + "F2F7": b"\x20\x00\x00\x00\x05\x31\x40\x06\x00\x40\x06\x00\x10\x03\x50\x20\x00\x00\x00\x00", + # Flicker Detection only: + "FD": b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60", +} + +# diff --git a/scripts/lib/bme680-2.0.0.dist-info/METADATA b/scripts/lib/bme680-2.0.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..52ebdfd830029a09573e9438c39696f15e452884 --- /dev/null +++ b/scripts/lib/bme680-2.0.0.dist-info/METADATA @@ -0,0 +1,156 @@ +Metadata-Version: 2.3 +Name: bme680 +Version: 2.0.0 +Summary: Python library for the BME680 temperature, humidity and gas sensor +Project-URL: GitHub, https://www.github.com/pimoroni/bme680-python +Project-URL: Homepage, https://www.pimoroni.com +Author-email: Philip Howard +Maintainer-email: Philip Howard +License: MIT License + + Copyright (c) 2018 Pimoroni Ltd + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +License-File: LICENSE +Keywords: Pi,Raspberry +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX :: Linux +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Topic :: Software Development +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: System :: Hardware +Requires-Python: >=3.7 +Requires-Dist: smbus2 +Description-Content-Type: text/markdown + +# BME680 + +[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme680-python/test.yml?branch=main)](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme680-python?branch=main) +[![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680) +[![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](https://pypi.python.org/pypi/bme680) + +https://shop.pimoroni.com/products/bme680 + +The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality. + +## Installing + +### Full install (recommended): + +We've created an easy installation script that will install all pre-requisites and get your BME680 +up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal +on your Raspberry Pi desktop, as illustrated below: + +![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) + +In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh +``` + +**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: + +``` +source ~/.virtualenvs/pimoroni/bin/activate +``` + +### Development: + +If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh --unstable +``` + +In all cases you will have to enable the i2c bus: + +``` +sudo raspi-config nonint do_i2c 0 +``` + +## Documentation & Support + +* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout +* Get help - http://forums.pimoroni.com/c/support + + +2.0.0 +----- + +* Repackage to hatch/pyproject.toml +* Drop Python 2.7 support +* Switch from smbu2 to smbus2 + +1.1.1 +----- + +* New: constants to clarify heater on/off states + +1.1.0 +----- + +* New: support for BME688 "high" gas resistance variant +* New: set/get gas heater disable bit +* Enhancement: fail with descriptive RuntimeError when chip is not detected + +1.0.5 +----- + +* New: set_temp_offset to calibrate temperature offset in degrees C + +1.0.4 +----- + +* Fix to range_sw_err for extremely high gas readings +* Convert to unsigned int to fix negative gas readings + +1.0.3 +----- + +* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3 + +1.0.2 +----- + +* Fixed set_gas_heater_temperature to avoid i2c TypeError + +1.0.1 +----- + +* Added Manifest to Python package + +1.0.0 +----- + +* Initial release + diff --git a/scripts/lib/bme680-2.0.0.dist-info/RECORD b/scripts/lib/bme680-2.0.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..35d5cc6b0bc1b4874b376ac990d1d4fb2a4b935c --- /dev/null +++ b/scripts/lib/bme680-2.0.0.dist-info/RECORD @@ -0,0 +1,7 @@ +CHANGELOG.md,, +LICENSE,, +README.md,, +bme680-2.0.0.dist-info/METADATA,, +bme680/__init__.py,, +bme680/constants.py,, +bme680-2.0.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/lib/bme680/__init__.py b/scripts/lib/bme680/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..56d547a19e9f2d520393977c910728528148c94e --- /dev/null +++ b/scripts/lib/bme680/__init__.py @@ -0,0 +1,486 @@ +"""BME680 Temperature, Pressure, Humidity & Gas Sensor.""" +import math +import time + +from . import constants +from .constants import BME680Data, lookupTable1, lookupTable2 + +__version__ = '2.0.0' + + +# Export constants to global namespace +# so end-users can "from BME680 import NAME" +if hasattr(constants, '__dict__'): + for key in constants.__dict__: + value = constants.__dict__[key] + if key not in globals(): + globals()[key] = value + + +class BME680(BME680Data): + """BOSCH BME680. + + Gas, pressure, temperature and humidity sensor. + + :param i2c_addr: One of I2C_ADDR_PRIMARY (0x76) or I2C_ADDR_SECONDARY (0x77) + :param i2c_device: Optional smbus or compatible instance for facilitating i2c communications. + + """ + + def __init__(self, i2c_addr=constants.I2C_ADDR_PRIMARY, i2c_device=None): + """Initialise BME680 sensor instance and verify device presence. + + :param i2c_addr: i2c address of BME680 + :param i2c_device: Optional SMBus-compatible instance for i2c transport + + """ + BME680Data.__init__(self) + + self.i2c_addr = i2c_addr + self._i2c = i2c_device + if self._i2c is None: + import smbus2 + self._i2c = smbus2.SMBus(1) + + try: + self.chip_id = self._get_regs(constants.CHIP_ID_ADDR, 1) + if self.chip_id != constants.CHIP_ID: + raise RuntimeError('BME680 Not Found. Invalid CHIP ID: 0x{0:02x}'.format(self.chip_id)) + except IOError: + raise RuntimeError("Unable to identify BME680 at 0x{:02x} (IOError)".format(self.i2c_addr)) + + self._variant = self._get_regs(constants.CHIP_VARIANT_ADDR, 1) + + self.soft_reset() + self.set_power_mode(constants.SLEEP_MODE) + + self._get_calibration_data() + + self.set_humidity_oversample(constants.OS_2X) + self.set_pressure_oversample(constants.OS_4X) + self.set_temperature_oversample(constants.OS_8X) + self.set_filter(constants.FILTER_SIZE_3) + if self._variant == constants.VARIANT_HIGH: + self.set_gas_status(constants.ENABLE_GAS_MEAS_HIGH) + else: + self.set_gas_status(constants.ENABLE_GAS_MEAS_LOW) + self.set_temp_offset(0) + self.get_sensor_data() + + def _get_calibration_data(self): + """Retrieve the sensor calibration data and store it in .calibration_data.""" + calibration = self._get_regs(constants.COEFF_ADDR1, constants.COEFF_ADDR1_LEN) + calibration += self._get_regs(constants.COEFF_ADDR2, constants.COEFF_ADDR2_LEN) + + heat_range = self._get_regs(constants.ADDR_RES_HEAT_RANGE_ADDR, 1) + heat_value = constants.twos_comp(self._get_regs(constants.ADDR_RES_HEAT_VAL_ADDR, 1), bits=8) + sw_error = constants.twos_comp(self._get_regs(constants.ADDR_RANGE_SW_ERR_ADDR, 1), bits=8) + + self.calibration_data.set_from_array(calibration) + self.calibration_data.set_other(heat_range, heat_value, sw_error) + + def soft_reset(self): + """Trigger a soft reset.""" + self._set_regs(constants.SOFT_RESET_ADDR, constants.SOFT_RESET_CMD) + time.sleep(constants.RESET_PERIOD / 1000.0) + + def set_temp_offset(self, value): + """Set temperature offset in celsius. + + If set, the temperature t_fine will be increased by given value in celsius. + :param value: Temperature offset in Celsius, eg. 4, -8, 1.25 + + """ + if value == 0: + self.offset_temp_in_t_fine = 0 + else: + self.offset_temp_in_t_fine = int(math.copysign((((int(abs(value) * 100)) << 8) - 128) / 5, value)) + + def set_humidity_oversample(self, value): + """Set humidity oversampling. + + A higher oversampling value means more stable sensor readings, + with less noise and jitter. + + However each step of oversampling adds about 2ms to the latency, + causing a slower response time to fast transients. + + :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X + + """ + self.tph_settings.os_hum = value + self._set_bits(constants.CONF_OS_H_ADDR, constants.OSH_MSK, constants.OSH_POS, value) + + def get_humidity_oversample(self): + """Get humidity oversampling.""" + return (self._get_regs(constants.CONF_OS_H_ADDR, 1) & constants.OSH_MSK) >> constants.OSH_POS + + def set_pressure_oversample(self, value): + """Set temperature oversampling. + + A higher oversampling value means more stable sensor readings, + with less noise and jitter. + + However each step of oversampling adds about 2ms to the latency, + causing a slower response time to fast transients. + + :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X + + """ + self.tph_settings.os_pres = value + self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OSP_MSK, constants.OSP_POS, value) + + def get_pressure_oversample(self): + """Get pressure oversampling.""" + return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OSP_MSK) >> constants.OSP_POS + + def set_temperature_oversample(self, value): + """Set pressure oversampling. + + A higher oversampling value means more stable sensor readings, + with less noise and jitter. + + However each step of oversampling adds about 2ms to the latency, + causing a slower response time to fast transients. + + :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X + + """ + self.tph_settings.os_temp = value + self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OST_MSK, constants.OST_POS, value) + + def get_temperature_oversample(self): + """Get temperature oversampling.""" + return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OST_MSK) >> constants.OST_POS + + def set_filter(self, value): + """Set IIR filter size. + + Optionally remove short term fluctuations from the temperature and pressure readings, + increasing their resolution but reducing their bandwidth. + + Enabling the IIR filter does not slow down the time a reading takes, but will slow + down the BME680s response to changes in temperature and pressure. + + When the IIR filter is enabled, the temperature and pressure resolution is effectively 20bit. + When it is disabled, it is 16bit + oversampling-1 bits. + + """ + self.tph_settings.filter = value + self._set_bits(constants.CONF_ODR_FILT_ADDR, constants.FILTER_MSK, constants.FILTER_POS, value) + + def get_filter(self): + """Get filter size.""" + return (self._get_regs(constants.CONF_ODR_FILT_ADDR, 1) & constants.FILTER_MSK) >> constants.FILTER_POS + + def select_gas_heater_profile(self, value): + """Set current gas sensor conversion profile. + + Select one of the 10 configured heating durations/set points. + + :param value: Profile index from 0 to 9 + + """ + if value > constants.NBCONV_MAX or value < constants.NBCONV_MIN: + raise ValueError("Profile '{}' should be between {} and {}".format(value, constants.NBCONV_MIN, constants.NBCONV_MAX)) + + self.gas_settings.nb_conv = value + self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.NBCONV_MSK, constants.NBCONV_POS, value) + + def get_gas_heater_profile(self): + """Get gas sensor conversion profile: 0 to 9.""" + return self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.NBCONV_MSK + + def set_gas_heater_status(self, value): + """Enable/disable gas heater.""" + self.gas_settings.heater = value + self._set_bits(constants.CONF_HEAT_CTRL_ADDR, constants.HCTRL_MSK, constants.HCTRL_POS, value) + + def get_gas_heater_status(self): + """Get current heater status.""" + return (self._get_regs(constants.CONF_HEAT_CTRL_ADDR, 1) & constants.HCTRL_MSK) >> constants.HCTRL_POS + + def set_gas_status(self, value): + """Enable/disable gas sensor.""" + if value == -1: + if self._variant == constants.VARIANT_HIGH: + value = constants.ENABLE_GAS_MEAS_HIGH + else: + value = constants.ENABLE_GAS_MEAS_LOW + self.gas_settings.run_gas = value + self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.RUN_GAS_MSK, constants.RUN_GAS_POS, value) + + def get_gas_status(self): + """Get the current gas status.""" + return (self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.RUN_GAS_MSK) >> constants.RUN_GAS_POS + + def set_gas_heater_profile(self, temperature, duration, nb_profile=0): + """Set temperature and duration of gas sensor heater. + + :param temperature: Target temperature in degrees celsius, between 200 and 400 + :param durarion: Target duration in milliseconds, between 1 and 4032 + :param nb_profile: Target profile, between 0 and 9 + + """ + self.set_gas_heater_temperature(temperature, nb_profile=nb_profile) + self.set_gas_heater_duration(duration, nb_profile=nb_profile) + + def set_gas_heater_temperature(self, value, nb_profile=0): + """Set gas sensor heater temperature. + + :param value: Target temperature in degrees celsius, between 200 and 400 + + When setting an nb_profile other than 0, + make sure to select it with select_gas_heater_profile. + + """ + if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: + raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) + + self.gas_settings.heatr_temp = value + temp = int(self._calc_heater_resistance(self.gas_settings.heatr_temp)) + self._set_regs(constants.RES_HEAT0_ADDR + nb_profile, temp) + + def set_gas_heater_duration(self, value, nb_profile=0): + """Set gas sensor heater duration. + + Heating durations between 1 ms and 4032 ms can be configured. + Approximately 20-30 ms are necessary for the heater to reach the intended target temperature. + + :param value: Heating duration in milliseconds. + + When setting an nb_profile other than 0, + make sure to select it with select_gas_heater_profile. + + """ + if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: + raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) + + self.gas_settings.heatr_dur = value + temp = self._calc_heater_duration(self.gas_settings.heatr_dur) + self._set_regs(constants.GAS_WAIT0_ADDR + nb_profile, temp) + + def set_power_mode(self, value, blocking=True): + """Set power mode.""" + if value not in (constants.SLEEP_MODE, constants.FORCED_MODE): + raise ValueError('Power mode should be one of SLEEP_MODE or FORCED_MODE') + + self.power_mode = value + + self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.MODE_MSK, constants.MODE_POS, value) + + while blocking and self.get_power_mode() != self.power_mode: + time.sleep(constants.POLL_PERIOD_MS / 1000.0) + + def get_power_mode(self): + """Get power mode.""" + self.power_mode = self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) + return self.power_mode + + def get_sensor_data(self): + """Get sensor data. + + Stores data in .data and returns True upon success. + + """ + self.set_power_mode(constants.FORCED_MODE) + + for attempt in range(10): + status = self._get_regs(constants.FIELD0_ADDR, 1) + + if (status & constants.NEW_DATA_MSK) == 0: + time.sleep(constants.POLL_PERIOD_MS / 1000.0) + continue + + regs = self._get_regs(constants.FIELD0_ADDR, constants.FIELD_LENGTH) + + self.data.status = regs[0] & constants.NEW_DATA_MSK + # Contains the nb_profile used to obtain the current measurement + self.data.gas_index = regs[0] & constants.GAS_INDEX_MSK + self.data.meas_index = regs[1] + + adc_pres = (regs[2] << 12) | (regs[3] << 4) | (regs[4] >> 4) + adc_temp = (regs[5] << 12) | (regs[6] << 4) | (regs[7] >> 4) + adc_hum = (regs[8] << 8) | regs[9] + adc_gas_res_low = (regs[13] << 2) | (regs[14] >> 6) + adc_gas_res_high = (regs[15] << 2) | (regs[16] >> 6) + gas_range_l = regs[14] & constants.GAS_RANGE_MSK + gas_range_h = regs[16] & constants.GAS_RANGE_MSK + + if self._variant == constants.VARIANT_HIGH: + self.data.status |= regs[16] & constants.GASM_VALID_MSK + self.data.status |= regs[16] & constants.HEAT_STAB_MSK + else: + self.data.status |= regs[14] & constants.GASM_VALID_MSK + self.data.status |= regs[14] & constants.HEAT_STAB_MSK + + self.data.heat_stable = (self.data.status & constants.HEAT_STAB_MSK) > 0 + + temperature = self._calc_temperature(adc_temp) + self.data.temperature = temperature / 100.0 + self.ambient_temperature = temperature # Saved for heater calc + + self.data.pressure = self._calc_pressure(adc_pres) / 100.0 + self.data.humidity = self._calc_humidity(adc_hum) / 1000.0 + + if self._variant == constants.VARIANT_HIGH: + self.data.gas_resistance = self._calc_gas_resistance_high(adc_gas_res_high, gas_range_h) + else: + self.data.gas_resistance = self._calc_gas_resistance_low(adc_gas_res_low, gas_range_l) + + return True + + return False + + def _set_bits(self, register, mask, position, value): + """Mask out and set one or more bits in a register.""" + temp = self._get_regs(register, 1) + temp &= ~mask + temp |= value << position + self._set_regs(register, temp) + + def _set_regs(self, register, value): + """Set one or more registers.""" + if isinstance(value, int): + self._i2c.write_byte_data(self.i2c_addr, register, value) + else: + self._i2c.write_i2c_block_data(self.i2c_addr, register, value) + + def _get_regs(self, register, length): + """Get one or more registers.""" + if length == 1: + return self._i2c.read_byte_data(self.i2c_addr, register) + else: + return self._i2c.read_i2c_block_data(self.i2c_addr, register, length) + + def _calc_temperature(self, temperature_adc): + """Convert the raw temperature to degrees C using calibration_data.""" + var1 = (temperature_adc >> 3) - (self.calibration_data.par_t1 << 1) + var2 = (var1 * self.calibration_data.par_t2) >> 11 + var3 = ((var1 >> 1) * (var1 >> 1)) >> 12 + var3 = ((var3) * (self.calibration_data.par_t3 << 4)) >> 14 + + # Save teperature data for pressure calculations + self.calibration_data.t_fine = (var2 + var3) + self.offset_temp_in_t_fine + calc_temp = (((self.calibration_data.t_fine * 5) + 128) >> 8) + + return calc_temp + + def _calc_pressure(self, pressure_adc): + """Convert the raw pressure using calibration data.""" + var1 = ((self.calibration_data.t_fine) >> 1) - 64000 + var2 = ((((var1 >> 2) * (var1 >> 2)) >> 11) * + self.calibration_data.par_p6) >> 2 + var2 = var2 + ((var1 * self.calibration_data.par_p5) << 1) + var2 = (var2 >> 2) + (self.calibration_data.par_p4 << 16) + var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) * + ((self.calibration_data.par_p3 << 5)) >> 3) + + ((self.calibration_data.par_p2 * var1) >> 1)) + var1 = var1 >> 18 + + var1 = ((32768 + var1) * self.calibration_data.par_p1) >> 15 + calc_pressure = 1048576 - pressure_adc + calc_pressure = ((calc_pressure - (var2 >> 12)) * (3125)) + + if calc_pressure >= (1 << 31): + calc_pressure = ((calc_pressure // var1) << 1) + else: + calc_pressure = ((calc_pressure << 1) // var1) + + var1 = (self.calibration_data.par_p9 * (((calc_pressure >> 3) * + (calc_pressure >> 3)) >> 13)) >> 12 + var2 = ((calc_pressure >> 2) * + self.calibration_data.par_p8) >> 13 + var3 = ((calc_pressure >> 8) * (calc_pressure >> 8) * + (calc_pressure >> 8) * + self.calibration_data.par_p10) >> 17 + + calc_pressure = (calc_pressure) + ((var1 + var2 + var3 + + (self.calibration_data.par_p7 << 7)) >> 4) + + return calc_pressure + + def _calc_humidity(self, humidity_adc): + """Convert the raw humidity using calibration data.""" + temp_scaled = ((self.calibration_data.t_fine * 5) + 128) >> 8 + var1 = (humidity_adc - ((self.calibration_data.par_h1 * 16))) -\ + (((temp_scaled * self.calibration_data.par_h3) // (100)) >> 1) + var2 = (self.calibration_data.par_h2 * + (((temp_scaled * self.calibration_data.par_h4) // (100)) + + (((temp_scaled * ((temp_scaled * self.calibration_data.par_h5) // (100))) >> 6) // + (100)) + (1 * 16384))) >> 10 + var3 = var1 * var2 + var4 = self.calibration_data.par_h6 << 7 + var4 = ((var4) + ((temp_scaled * self.calibration_data.par_h7) // (100))) >> 4 + var5 = ((var3 >> 14) * (var3 >> 14)) >> 10 + var6 = (var4 * var5) >> 1 + calc_hum = (((var3 + var6) >> 10) * (1000)) >> 12 + + return min(max(calc_hum, 0), 100000) + + def _calc_gas_resistance(self, gas_res_adc, gas_range): + """Convert the raw gas resistance using calibration data.""" + if self._variant == constants.VARIANT_HIGH: + return self._calc_gas_resistance_high(gas_res_adc, gas_range) + else: + return self._calc_gas_resistance_low(gas_res_adc, gas_range) + + def _calc_gas_resistance_high(self, gas_res_adc, gas_range): + """Convert the raw gas resistance using calibration data. + + Applies to Variant ID == 0x01 only. + + """ + var1 = 262144 >> gas_range + var2 = gas_res_adc - 512 + + var2 *= 3 + var2 = 4096 + var2 + + calc_gas_res = (10000 * var1) / var2 + calc_gas_res *= 100 + + return calc_gas_res + + def _calc_gas_resistance_low(self, gas_res_adc, gas_range): + """Convert the raw gas resistance using calibration data. + + Applies to Variant ID == 0x00 only. + + """ + var1 = ((1340 + (5 * self.calibration_data.range_sw_err)) * (lookupTable1[gas_range])) >> 16 + var2 = (((gas_res_adc << 15) - (16777216)) + var1) + var3 = ((lookupTable2[gas_range] * var1) >> 9) + calc_gas_res = ((var3 + (var2 >> 1)) / var2) + + if calc_gas_res < 0: + calc_gas_res = (1 << 32) + calc_gas_res + + return calc_gas_res + + def _calc_heater_resistance(self, temperature): + """Convert raw heater resistance using calibration data.""" + temperature = min(max(temperature, 200), 400) + + var1 = ((self.ambient_temperature * self.calibration_data.par_gh3) / 1000) * 256 + var2 = (self.calibration_data.par_gh1 + 784) * (((((self.calibration_data.par_gh2 + 154009) * temperature * 5) / 100) + 3276800) / 10) + var3 = var1 + (var2 / 2) + var4 = (var3 / (self.calibration_data.res_heat_range + 4)) + var5 = (131 * self.calibration_data.res_heat_val) + 65536 + heatr_res_x100 = (((var4 / var5) - 250) * 34) + heatr_res = ((heatr_res_x100 + 50) / 100) + + return heatr_res + + def _calc_heater_duration(self, duration): + """Calculate correct value for heater duration setting from milliseconds.""" + if duration < 0xfc0: + factor = 0 + + while duration > 0x3f: + duration /= 4 + factor += 1 + + return int(duration + (factor * 64)) + + return 0xff diff --git a/scripts/lib/bme680/constants.py b/scripts/lib/bme680/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..d77415d3e79f9ac5bda287380eaad67a67d6776f --- /dev/null +++ b/scripts/lib/bme680/constants.py @@ -0,0 +1,413 @@ +"""BME680 constants, structures and utilities.""" + +# BME680 General config +POLL_PERIOD_MS = 10 + +# BME680 I2C addresses +I2C_ADDR_PRIMARY = 0x76 +I2C_ADDR_SECONDARY = 0x77 + +# BME680 unique chip identifier +CHIP_ID = 0x61 + +# BME680 coefficients related defines +COEFF_SIZE = 41 +COEFF_ADDR1_LEN = 25 +COEFF_ADDR2_LEN = 16 + +# BME680 field_x related defines +FIELD_LENGTH = 17 +FIELD_ADDR_OFFSET = 17 + +# Soft reset command +SOFT_RESET_CMD = 0xb6 + +# Error code definitions +OK = 0 +# Errors +E_NULL_PTR = -1 +E_COM_FAIL = -2 +E_DEV_NOT_FOUND = -3 +E_INVALID_LENGTH = -4 + +# Warnings +W_DEFINE_PWR_MODE = 1 +W_NO_NEW_DATA = 2 + +# Info's +I_MIN_CORRECTION = 1 +I_MAX_CORRECTION = 2 + +# Register map +# Other coefficient's address +ADDR_RES_HEAT_VAL_ADDR = 0x00 +ADDR_RES_HEAT_RANGE_ADDR = 0x02 +ADDR_RANGE_SW_ERR_ADDR = 0x04 +ADDR_SENS_CONF_START = 0x5A +ADDR_GAS_CONF_START = 0x64 + +# Field settings +FIELD0_ADDR = 0x1d + +# Heater settings +RES_HEAT0_ADDR = 0x5a +GAS_WAIT0_ADDR = 0x64 + +# Sensor configuration registers +CONF_HEAT_CTRL_ADDR = 0x70 +CONF_ODR_RUN_GAS_NBC_ADDR = 0x71 +CONF_OS_H_ADDR = 0x72 +MEM_PAGE_ADDR = 0xf3 +CONF_T_P_MODE_ADDR = 0x74 +CONF_ODR_FILT_ADDR = 0x75 + +# Coefficient's address +COEFF_ADDR1 = 0x89 +COEFF_ADDR2 = 0xe1 + +# Chip identifier +CHIP_ID_ADDR = 0xd0 +CHIP_VARIANT_ADDR = 0xf0 + +VARIANT_LOW = 0x00 +VARIANT_HIGH = 0x01 + +# Soft reset register +SOFT_RESET_ADDR = 0xe0 + +# Heater control settings +ENABLE_HEATER = 0x00 +DISABLE_HEATER = 0x08 + +# Gas measurement settings +DISABLE_GAS_MEAS = 0x00 +ENABLE_GAS_MEAS = -1 # Now used as auto-select +ENABLE_GAS_MEAS_LOW = 0x01 +ENABLE_GAS_MEAS_HIGH = 0x02 + +# Over-sampling settings +OS_NONE = 0 +OS_1X = 1 +OS_2X = 2 +OS_4X = 3 +OS_8X = 4 +OS_16X = 5 + +# IIR filter settings +FILTER_SIZE_0 = 0 +FILTER_SIZE_1 = 1 +FILTER_SIZE_3 = 2 +FILTER_SIZE_7 = 3 +FILTER_SIZE_15 = 4 +FILTER_SIZE_31 = 5 +FILTER_SIZE_63 = 6 +FILTER_SIZE_127 = 7 + +# Power mode settings +SLEEP_MODE = 0 +FORCED_MODE = 1 + +# Delay related macro declaration +RESET_PERIOD = 10 + +# SPI memory page settings +MEM_PAGE0 = 0x10 +MEM_PAGE1 = 0x00 + +# Ambient humidity shift value for compensation +HUM_REG_SHIFT_VAL = 4 + +# Run gas enable and disable settings +RUN_GAS_DISABLE = 0 +RUN_GAS_ENABLE = 1 + +# Gas heater enable and disable settings +GAS_HEAT_ENABLE = 0 +GAS_HEAT_DISABLE = 1 + +# Buffer length macro declaration +TMP_BUFFER_LENGTH = 40 +REG_BUFFER_LENGTH = 6 +FIELD_DATA_LENGTH = 3 +GAS_REG_BUF_LENGTH = 20 +GAS_HEATER_PROF_LEN_MAX = 10 + +# Settings selector +OST_SEL = 1 +OSP_SEL = 2 +OSH_SEL = 4 +GAS_MEAS_SEL = 8 +FILTER_SEL = 16 +HCNTRL_SEL = 32 +RUN_GAS_SEL = 64 +NBCONV_SEL = 128 +GAS_SENSOR_SEL = GAS_MEAS_SEL | RUN_GAS_SEL | NBCONV_SEL + +# Number of conversion settings +NBCONV_MIN = 0 +NBCONV_MAX = 9 # Was 10, but there are only 10 settings: 0 1 2 ... 8 9 + +# Mask definitions +GAS_MEAS_MSK = 0x30 +NBCONV_MSK = 0X0F +FILTER_MSK = 0X1C +OST_MSK = 0XE0 +OSP_MSK = 0X1C +OSH_MSK = 0X07 +HCTRL_MSK = 0x08 +RUN_GAS_MSK = 0x30 +MODE_MSK = 0x03 +RHRANGE_MSK = 0x30 +RSERROR_MSK = 0xf0 +NEW_DATA_MSK = 0x80 +GAS_INDEX_MSK = 0x0f +GAS_RANGE_MSK = 0x0f +GASM_VALID_MSK = 0x20 +HEAT_STAB_MSK = 0x10 +MEM_PAGE_MSK = 0x10 +SPI_RD_MSK = 0x80 +SPI_WR_MSK = 0x7f +BIT_H1_DATA_MSK = 0x0F + +# Bit position definitions for sensor settings +GAS_MEAS_POS = 4 +FILTER_POS = 2 +OST_POS = 5 +OSP_POS = 2 +OSH_POS = 0 +HCTRL_POS = 3 +RUN_GAS_POS = 4 +MODE_POS = 0 +NBCONV_POS = 0 + +# Array Index to Field data mapping for Calibration Data +T2_LSB_REG = 1 +T2_MSB_REG = 2 +T3_REG = 3 +P1_LSB_REG = 5 +P1_MSB_REG = 6 +P2_LSB_REG = 7 +P2_MSB_REG = 8 +P3_REG = 9 +P4_LSB_REG = 11 +P4_MSB_REG = 12 +P5_LSB_REG = 13 +P5_MSB_REG = 14 +P7_REG = 15 +P6_REG = 16 +P8_LSB_REG = 19 +P8_MSB_REG = 20 +P9_LSB_REG = 21 +P9_MSB_REG = 22 +P10_REG = 23 +H2_MSB_REG = 25 +H2_LSB_REG = 26 +H1_LSB_REG = 26 +H1_MSB_REG = 27 +H3_REG = 28 +H4_REG = 29 +H5_REG = 30 +H6_REG = 31 +H7_REG = 32 +T1_LSB_REG = 33 +T1_MSB_REG = 34 +GH2_LSB_REG = 35 +GH2_MSB_REG = 36 +GH1_REG = 37 +GH3_REG = 38 + +# BME680 register buffer index settings +REG_FILTER_INDEX = 5 +REG_TEMP_INDEX = 4 +REG_PRES_INDEX = 4 +REG_HUM_INDEX = 2 +REG_NBCONV_INDEX = 1 +REG_RUN_GAS_INDEX = 1 +REG_HCTRL_INDEX = 0 + +# Look up tables for the possible gas range values +lookupTable1 = [2147483647, 2147483647, 2147483647, 2147483647, + 2147483647, 2126008810, 2147483647, 2130303777, 2147483647, + 2147483647, 2143188679, 2136746228, 2147483647, 2126008810, + 2147483647, 2147483647] + +lookupTable2 = [4096000000, 2048000000, 1024000000, 512000000, + 255744255, 127110228, 64000000, 32258064, + 16016016, 8000000, 4000000, 2000000, + 1000000, 500000, 250000, 125000] + + +def bytes_to_word(msb, lsb, bits=16, signed=False): + """Convert a most and least significant byte into a word.""" + # TODO: Reimplement with struct + word = (msb << 8) | lsb + if signed: + word = twos_comp(word, bits) + return word + + +def twos_comp(val, bits=16): + """Convert two bytes into a two's compliment signed word.""" + # TODO: Reimplement with struct + if val & (1 << (bits - 1)) != 0: + val = val - (1 << bits) + return val + + +class FieldData: + """Structure for storing BME680 sensor data.""" + + def __init__(self): # noqa D107 + # Contains new_data, gasm_valid & heat_stab + self.status = None + self.heat_stable = False + # The index of the heater profile used + self.gas_index = None + # Measurement index to track order + self.meas_index = None + # Temperature in degree celsius x100 + self.temperature = None + # Pressure in Pascal + self.pressure = None + # Humidity in % relative humidity x1000 + self.humidity = None + # Gas resistance in Ohms + self.gas_resistance = None + + +class CalibrationData: + """Structure for storing BME680 calibration data.""" + + def __init__(self): # noqa D107 + self.par_h1 = None + self.par_h2 = None + self.par_h3 = None + self.par_h4 = None + self.par_h5 = None + self.par_h6 = None + self.par_h7 = None + self.par_gh1 = None + self.par_gh2 = None + self.par_gh3 = None + self.par_t1 = None + self.par_t2 = None + self.par_t3 = None + self.par_p1 = None + self.par_p2 = None + self.par_p3 = None + self.par_p4 = None + self.par_p5 = None + self.par_p6 = None + self.par_p7 = None + self.par_p8 = None + self.par_p9 = None + self.par_p10 = None + # Variable to store t_fine size + self.t_fine = None + # Variable to store heater resistance range + self.res_heat_range = None + # Variable to store heater resistance value + self.res_heat_val = None + # Variable to store error range + self.range_sw_err = None + + def set_from_array(self, calibration): + """Set parameters from an array of bytes.""" + # Temperature related coefficients + self.par_t1 = bytes_to_word(calibration[T1_MSB_REG], calibration[T1_LSB_REG]) + self.par_t2 = bytes_to_word(calibration[T2_MSB_REG], calibration[T2_LSB_REG], bits=16, signed=True) + self.par_t3 = twos_comp(calibration[T3_REG], bits=8) + + # Pressure related coefficients + self.par_p1 = bytes_to_word(calibration[P1_MSB_REG], calibration[P1_LSB_REG]) + self.par_p2 = bytes_to_word(calibration[P2_MSB_REG], calibration[P2_LSB_REG], bits=16, signed=True) + self.par_p3 = twos_comp(calibration[P3_REG], bits=8) + self.par_p4 = bytes_to_word(calibration[P4_MSB_REG], calibration[P4_LSB_REG], bits=16, signed=True) + self.par_p5 = bytes_to_word(calibration[P5_MSB_REG], calibration[P5_LSB_REG], bits=16, signed=True) + self.par_p6 = twos_comp(calibration[P6_REG], bits=8) + self.par_p7 = twos_comp(calibration[P7_REG], bits=8) + self.par_p8 = bytes_to_word(calibration[P8_MSB_REG], calibration[P8_LSB_REG], bits=16, signed=True) + self.par_p9 = bytes_to_word(calibration[P9_MSB_REG], calibration[P9_LSB_REG], bits=16, signed=True) + self.par_p10 = calibration[P10_REG] + + # Humidity related coefficients + self.par_h1 = (calibration[H1_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H1_LSB_REG] & BIT_H1_DATA_MSK) + self.par_h2 = (calibration[H2_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H2_LSB_REG] >> HUM_REG_SHIFT_VAL) + self.par_h3 = twos_comp(calibration[H3_REG], bits=8) + self.par_h4 = twos_comp(calibration[H4_REG], bits=8) + self.par_h5 = twos_comp(calibration[H5_REG], bits=8) + self.par_h6 = calibration[H6_REG] + self.par_h7 = twos_comp(calibration[H7_REG], bits=8) + + # Gas heater related coefficients + self.par_gh1 = twos_comp(calibration[GH1_REG], bits=8) + self.par_gh2 = bytes_to_word(calibration[GH2_MSB_REG], calibration[GH2_LSB_REG], bits=16, signed=True) + self.par_gh3 = twos_comp(calibration[GH3_REG], bits=8) + + def set_other(self, heat_range, heat_value, sw_error): + """Set other values.""" + self.res_heat_range = (heat_range & RHRANGE_MSK) // 16 + self.res_heat_val = heat_value + self.range_sw_err = (sw_error & RSERROR_MSK) // 16 + + +class TPHSettings: + """Structure for storing BME680 sensor settings. + + Comprises of output data rate, over-sampling and filter settings. + + """ + + def __init__(self): # noqa D107 + # Humidity oversampling + self.os_hum = None + # Temperature oversampling + self.os_temp = None + # Pressure oversampling + self.os_pres = None + # Filter coefficient + self.filter = None + + +class GasSettings: + """Structure for storing BME680 gas settings and status.""" + + def __init__(self): # noqa D107 + # Variable to store nb conversion + self.nb_conv = None + # Variable to store heater control + self.heatr_ctrl = None + # Run gas enable value + self.run_gas = None + # Pointer to store heater temperature + self.heatr_temp = None + # Pointer to store duration profile + self.heatr_dur = None + + +class BME680Data: + """Structure to represent BME680 device.""" + + def __init__(self): # noqa D107 + # Chip Id + self.chip_id = None + # Device Id + self.dev_id = None + # SPI/I2C interface + self.intf = None + # Memory page used + self.mem_page = None + # Ambient temperature in Degree C + self.ambient_temperature = None + # Field Data + self.data = FieldData() + # Sensor calibration data + self.calibration_data = CalibrationData() + # Sensor settings + self.tph_settings = TPHSettings() + # Gas Sensor settings + self.gas_settings = GasSettings() + # Sensor power modes + self.power_mode = None + # New sensor fields + self.new_fields = None diff --git a/scripts/lib/data_logging.py b/scripts/lib/data_logging.py new file mode 100644 index 0000000000000000000000000000000000000000..f391682db684f3f8ac8f035b013969b4e7e5e4e3 --- /dev/null +++ b/scripts/lib/data_logging.py @@ -0,0 +1,166 @@ +import json +import sys +from time import gmtime, localtime, time + +import machine +import ntptime +import uos +import urequests +from machine import SPI, Pin +from sdcard import sdcard +from uio import StringIO + +# # uses a more robust ntptime +# from lib.ntptime import ntptime + + +def get_traceback(err): + try: + with StringIO() as f: # type: ignore + sys.print_exception(err, f) + return f.getvalue() + except Exception as err2: + print(err2) + return f"Failed to extract file and line number due to {err2}.\nOriginal error: {err}" # noqa: E501 + + +def initialize_sdcard( + spi_id=1, + cs_pin=15, + sck_pin=10, + mosi_pin=11, + miso_pin=12, + baudrate=1000000, + polarity=0, + phase=0, + bits=8, + firstbit=SPI.MSB, + verbose=True, +): + try: + cs = Pin(cs_pin, Pin.OUT) + + spi = SPI( + spi_id, + baudrate=baudrate, + polarity=polarity, + phase=phase, + bits=bits, + firstbit=firstbit, + sck=Pin(sck_pin), + mosi=Pin(mosi_pin), + miso=Pin(miso_pin), + ) + + # Initialize SD card + sd = sdcard.SDCard(spi, cs) + + vfs = uos.VfsFat(sd) + uos.mount(vfs, "/sd") # type: ignore + if verbose: + print("SD Card initialized successfully") + return True + except Exception as e: + if verbose: + print(get_traceback(e)) + print("SD Card failed to initialize") + return False + + +def write_payload_backup(payload_data: str, fpath: str = "/sd/experiments.txt"): + payload = json.dumps(payload_data) + with open(fpath, "a") as file: + # line = ",".join([str(payload[key]) for key in payload.keys()]) + file.write(f"{payload}\r\n") + + +def log_to_mongodb( + document: dict, + api_key: str, + url: str, + cluster_name: str, + database_name: str, + collection_name: str, + verbose: bool = True, + retries: int = 2, +): + # based on https://medium.com/@johnlpage/introduction-to-microcontrollers-and-the-pi-pico-w-f7a2d9ad1394 + headers = {"api-key": api_key} + + insertPayload = { + "dataSource": cluster_name, + "database": database_name, + "collection": collection_name, + "document": document, + } + + if verbose: + print(f"sending document to {cluster_name}:{database_name}:{collection_name}") + + for _ in range(retries): + response = None + if _ > 0: + print(f"retrying... ({_} of {retries})") + + try: + response = urequests.post(url, headers=headers, json=insertPayload) + txt = str(response.text) + status_code = response.status_code + + if verbose: + print(f"Response: ({status_code}), msg = {txt}") + if response.status_code == 201: + print("Added Successfully") + break + else: + print("Error") + + # Always close response objects so we don't leak memory + response.close() + except Exception as e: + if response is not None: + response.close() + if _ == retries - 1: + raise e + else: + print(e) + + +def get_timestamp(timeout=2, return_str=False): + ntptime.timeout = timeout # type: ignore + time_int = ntptime.time() + utc_tuple = gmtime(time_int) + year, month, mday, hour, minute, second, weekday, yearday = utc_tuple + + time_str = f"{year}-{month}-{mday} {hour:02}:{minute:02}:{second:02}" + + if return_str: + return time_int, time_str + + return time_int + + +def get_local_timestamp(return_str=False): + t = time() + year, month, mday, hour, minute, second, _, _ = localtime(t) + time_str = f"{year}-{month}-{mday} {hour:02}:{minute:02}:{second:02}" + + if return_str: + return t, time_str + + return t + + +def get_onboard_temperature(unit="K"): + sensor_temp = machine.ADC(4) + conversion_factor = 3.3 / (65535) + reading = sensor_temp.read_u16() * conversion_factor + celsius_degrees = 27 - (reading - 0.706) / 0.001721 + if unit == "C": + return celsius_degrees + elif unit == "K": + return celsius_degrees + 273.15 + elif unit == "F": + return celsius_degrees * 9 / 5 + 32 + else: + raise ValueError("Invalid unit. Must be one of 'C', 'K', or 'F") diff --git a/scripts/lib/functools.py b/scripts/lib/functools.py new file mode 100644 index 0000000000000000000000000000000000000000..510a3406f9580606d9bad378315104a0fcc91cfa --- /dev/null +++ b/scripts/lib/functools.py @@ -0,0 +1,28 @@ +def partial(func, *args, **kwargs): + def _partial(*more_args, **more_kwargs): + kw = kwargs.copy() + kw.update(more_kwargs) + func(*(args + more_args), **kw) + + return _partial + + +def update_wrapper(wrapper, wrapped): + # Dummy impl + return wrapper + + +def wraps(wrapped): + # Dummy impl + return lambda x: x + + +def reduce(function, iterable, initializer=None): + it = iter(iterable) + if initializer is None: + value = next(it) + else: + value = initializer + for element in it: + value = function(value, element) + return value diff --git a/scripts/lib/mqtt_as.py b/scripts/lib/mqtt_as.py new file mode 100644 index 0000000000000000000000000000000000000000..2be2cb50548a85374f138994aff792431f3f16ad --- /dev/null +++ b/scripts/lib/mqtt_as.py @@ -0,0 +1,824 @@ +# mqtt_as.py Asynchronous version of umqtt.robust +# (C) Copyright Peter Hinch 2017-2023. +# Released under the MIT licence. + +# Pyboard D support added also RP2/default +# Various improvements contributed by Kevin Köck. + +import gc + +import usocket as socket +import ustruct as struct + +gc.collect() +import uasyncio as asyncio +from ubinascii import hexlify + +gc.collect() +from uerrno import EINPROGRESS, ETIMEDOUT +from utime import ticks_diff, ticks_ms + +gc.collect() +import network +from machine import unique_id +from micropython import const + +gc.collect() +from sys import platform + +VERSION = (0, 7, 1) + +# Default short delay for good SynCom throughput (avoid sleep(0) with SynCom). +_DEFAULT_MS = const(20) +_SOCKET_POLL_DELAY = const(5) # 100ms added greatly to publish latency + +# Legitimate errors while waiting on a socket. See uasyncio __init__.py open_connection(). +ESP32 = platform == "esp32" +RP2 = platform == "rp2" +if ESP32: + # https://forum.micropython.org/viewtopic.php?f=16&t=3608&p=20942#p20942 + BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, 118, 119] # Add in weird ESP32 errors +elif RP2: + BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, -110] +else: + BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT] + +ESP8266 = platform == "esp8266" +PYBOARD = platform == "pyboard" + + +# Default "do little" coro for optional user replacement +async def eliza(*_): # e.g. via set_wifi_handler(coro): see test program + await asyncio.sleep_ms(_DEFAULT_MS) + + +class MsgQueue: + def __init__(self, size): + self._q = [0 for _ in range(max(size, 4))] + self._size = size + self._wi = 0 + self._ri = 0 + self._evt = asyncio.Event() + self.discards = 0 + + def put(self, *v): + self._q[self._wi] = v + self._evt.set() + self._wi = (self._wi + 1) % self._size + if self._wi == self._ri: # Would indicate empty + self._ri = (self._ri + 1) % self._size # Discard a message + self.discards += 1 + + def __aiter__(self): + return self + + async def __anext__(self): + if self._ri == self._wi: # Empty + self._evt.clear() + await self._evt.wait() + r = self._q[self._ri] + self._ri = (self._ri + 1) % self._size + return r + + +config = { + "client_id": hexlify(unique_id()), + "server": None, + "port": 0, + "user": "", + "password": "", + "keepalive": 60, + "ping_interval": 0, + "ssl": False, + "ssl_params": {}, + "response_time": 10, + "clean_init": True, + "clean": True, + "max_repubs": 4, + "will": None, + "subs_cb": lambda *_: None, + "wifi_coro": eliza, + "connect_coro": eliza, + "ssid": None, + "wifi_pw": None, + "queue_len": 0, + "gateway": False, +} + + +class MQTTException(Exception): + pass + + +def pid_gen(): + pid = 0 + while True: + pid = pid + 1 if pid < 65535 else 1 + yield pid + + +def qos_check(qos): + if not (qos == 0 or qos == 1): + raise ValueError("Only qos 0 and 1 are supported.") + + +# MQTT_base class. Handles MQTT protocol on the basis of a good connection. +# Exceptions from connectivity failures are handled by MQTTClient subclass. +class MQTT_base: + REPUB_COUNT = 0 # TEST + DEBUG = False + + def __init__(self, config): + self._events = config["queue_len"] > 0 + # MQTT config + self._client_id = config["client_id"] + self._user = config["user"] + self._pswd = config["password"] + self._keepalive = config["keepalive"] + if self._keepalive >= 65536: + raise ValueError("invalid keepalive time") + self._response_time = ( + config["response_time"] * 1000 + ) # Repub if no PUBACK received (ms). + self._max_repubs = config["max_repubs"] + self._clean_init = config[ + "clean_init" + ] # clean_session state on first connection + self._clean = config["clean"] # clean_session state on reconnect + will = config["will"] + if will is None: + self._lw_topic = False + else: + self._set_last_will(*will) + # WiFi config + self._ssid = config["ssid"] # Required for ESP32 / Pyboard D. Optional ESP8266 + self._wifi_pw = config["wifi_pw"] + self._ssl = config["ssl"] + self._ssl_params = config["ssl_params"] + # Callbacks and coros + if self._events: + self.up = asyncio.Event() + self.down = asyncio.Event() + self.queue = MsgQueue(config["queue_len"]) + else: # Callbacks + self._cb = config["subs_cb"] + self._wifi_handler = config["wifi_coro"] + self._connect_handler = config["connect_coro"] + # Network + self.port = config["port"] + if self.port == 0: + self.port = 8883 if self._ssl else 1883 + self.server = config["server"] + if self.server is None: + raise ValueError("no server specified.") + self._sock = None + self._sta_if = network.WLAN(network.STA_IF) + self._sta_if.active(True) + if config["gateway"]: # Called from gateway (hence ESP32). + import aioespnow # Set up ESPNOW + + while not (sta := self._sta_if).active(): + time.sleep(0.1) + sta.config(pm=sta.PM_NONE) # No power management + sta.active(True) + self._espnow = ( + aioespnow.AIOESPNow() + ) # Returns AIOESPNow enhanced with async support + self._espnow.active(True) + + self.newpid = pid_gen() + self.rcv_pids = set() # PUBACK and SUBACK pids awaiting ACK response + self.last_rx = ticks_ms() # Time of last communication from broker + self.lock = asyncio.Lock() + + def _set_last_will(self, topic, msg, retain=False, qos=0): + qos_check(qos) + if not topic: + raise ValueError("Empty topic.") + self._lw_topic = topic + self._lw_msg = msg + self._lw_qos = qos + self._lw_retain = retain + + def dprint(self, msg, *args): + if self.DEBUG: + print(msg % args) + + def _timeout(self, t): + return ticks_diff(ticks_ms(), t) > self._response_time + + async def _as_read(self, n, sock=None): # OSError caught by superclass + if sock is None: + sock = self._sock + # Declare a byte array of size n. That space is needed anyway, better + # to just 'allocate' it in one go instead of appending to an + # existing object, this prevents reallocation and fragmentation. + data = bytearray(n) + buffer = memoryview(data) + size = 0 + t = ticks_ms() + while size < n: + if self._timeout(t) or not self.isconnected(): + raise OSError(-1, "Timeout on socket read") + try: + msg_size = sock.readinto(buffer[size:], n - size) + except OSError as e: # ESP32 issues weird 119 errors here + msg_size = None + if e.args[0] not in BUSY_ERRORS: + raise + if msg_size == 0: # Connection closed by host + raise OSError(-1, "Connection closed by host") + if msg_size is not None: # data received + size += msg_size + t = ticks_ms() + self.last_rx = ticks_ms() + await asyncio.sleep_ms(_SOCKET_POLL_DELAY) + return data + + async def _as_write(self, bytes_wr, length=0, sock=None): + if sock is None: + sock = self._sock + + # Wrap bytes in memoryview to avoid copying during slicing + bytes_wr = memoryview(bytes_wr) + if length: + bytes_wr = bytes_wr[:length] + t = ticks_ms() + while bytes_wr: + if self._timeout(t) or not self.isconnected(): + raise OSError(-1, "Timeout on socket write") + try: + n = sock.write(bytes_wr) + except OSError as e: # ESP32 issues weird 119 errors here + n = 0 + if e.args[0] not in BUSY_ERRORS: + raise + if n: + t = ticks_ms() + bytes_wr = bytes_wr[n:] + await asyncio.sleep_ms(_SOCKET_POLL_DELAY) + + async def _send_str(self, s): + await self._as_write(struct.pack("!H", len(s))) + await self._as_write(s) + + async def _recv_len(self): + n = 0 + sh = 0 + while 1: + res = await self._as_read(1) + b = res[0] + n |= (b & 0x7F) << sh + if not b & 0x80: + return n + sh += 7 + + async def _connect(self, clean): + self._sock = socket.socket() + self._sock.setblocking(False) + try: + self._sock.connect(self._addr) + except OSError as e: + if e.args[0] not in BUSY_ERRORS: + raise + await asyncio.sleep_ms(_DEFAULT_MS) + self.dprint("Connecting to broker.") + if self._ssl: + import ssl + + self._sock = ssl.wrap_socket(self._sock, **self._ssl_params) + premsg = bytearray(b"\x10\0\0\0\0\0") + msg = bytearray(b"\x04MQTT\x04\0\0\0") # Protocol 3.1.1 + + sz = 10 + 2 + len(self._client_id) + msg[6] = clean << 1 + if self._user: + sz += 2 + len(self._user) + 2 + len(self._pswd) + msg[6] |= 0xC0 + if self._keepalive: + msg[7] |= self._keepalive >> 8 + msg[8] |= self._keepalive & 0x00FF + if self._lw_topic: + sz += 2 + len(self._lw_topic) + 2 + len(self._lw_msg) + msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3 + msg[6] |= self._lw_retain << 5 + + i = 1 + while sz > 0x7F: + premsg[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + premsg[i] = sz + await self._as_write(premsg, i + 2) + await self._as_write(msg) + await self._send_str(self._client_id) + if self._lw_topic: + await self._send_str(self._lw_topic) + await self._send_str(self._lw_msg) + if self._user: + await self._send_str(self._user) + await self._send_str(self._pswd) + # Await CONNACK + # read causes ECONNABORTED if broker is out; triggers a reconnect. + resp = await self._as_read(4) + self.dprint("Connected to broker.") # Got CONNACK + if ( + resp[3] != 0 or resp[0] != 0x20 or resp[1] != 0x02 + ): # Bad CONNACK e.g. authentication fail. + raise OSError( + -1, + f"Connect fail: 0x{(resp[0] << 8) + resp[1]:04x} {resp[3]} (README 7)", + ) + + async def _ping(self): + async with self.lock: + await self._as_write(b"\xc0\0") + + # Check internet connectivity by sending DNS lookup to Google's 8.8.8.8 + async def wan_ok( + self, + packet=b"$\x1a\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01", + ): + if not self.isconnected(): # WiFi is down + return False + length = 32 # DNS query and response packet size + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setblocking(False) + s.connect(("8.8.8.8", 53)) + await asyncio.sleep(1) + try: + await self._as_write(packet, sock=s) + await asyncio.sleep(2) + res = await self._as_read(length, s) + if len(res) == length: + return True # DNS response size OK + except OSError: # Timeout on read: no connectivity. + return False + finally: + s.close() + return False + + async def broker_up(self): # Test broker connectivity + if not self.isconnected(): + return False + tlast = self.last_rx + if ticks_diff(ticks_ms(), tlast) < 1000: + return True + try: + await self._ping() + except OSError: + return False + t = ticks_ms() + while not self._timeout(t): + await asyncio.sleep_ms(100) + if ticks_diff(self.last_rx, tlast) > 0: # Response received + return True + return False + + async def disconnect(self): + if self._sock is not None: + await self._kill_tasks(False) # Keep socket open + try: + async with self.lock: + self._sock.write(b"\xe0\0") # Close broker connection + await asyncio.sleep_ms(100) + except OSError: + pass + self._close() + self._has_connected = False + + def _close(self): + if self._sock is not None: + self._sock.close() + + def close( + self, + ): # API. See https://github.com/peterhinch/micropython-mqtt/issues/60 + self._close() + try: + self._sta_if.disconnect() # Disconnect Wi-Fi to avoid errors + except OSError: + self.dprint("Wi-Fi not started, unable to disconnect interface") + self._sta_if.active(False) + + async def _await_pid(self, pid): + t = ticks_ms() + while pid in self.rcv_pids: # local copy + if self._timeout(t) or not self.isconnected(): + break # Must repub or bail out + await asyncio.sleep_ms(100) + else: + return True # PID received. All done. + return False + + # qos == 1: coro blocks until wait_msg gets correct PID. + # If WiFi fails completely subclass re-publishes with new PID. + async def publish(self, topic, msg, retain, qos): + pid = next(self.newpid) + if qos: + self.rcv_pids.add(pid) + async with self.lock: + await self._publish(topic, msg, retain, qos, 0, pid) + if qos == 0: + return + + count = 0 + while 1: # Await PUBACK, republish on timeout + if await self._await_pid(pid): + return + # No match + if count >= self._max_repubs or not self.isconnected(): + raise OSError(-1) # Subclass to re-publish with new PID + async with self.lock: + await self._publish(topic, msg, retain, qos, dup=1, pid=pid) # Add pid + count += 1 + self.REPUB_COUNT += 1 + + async def _publish(self, topic, msg, retain, qos, dup, pid): + pkt = bytearray(b"\x30\0\0\0") + pkt[0] |= qos << 1 | retain | dup << 3 + sz = 2 + len(topic) + len(msg) + if qos > 0: + sz += 2 + if sz >= 2097152: + raise MQTTException("Strings too long.") + i = 1 + while sz > 0x7F: + pkt[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + pkt[i] = sz + await self._as_write(pkt, i + 1) + await self._send_str(topic) + if qos > 0: + struct.pack_into("!H", pkt, 0, pid) + await self._as_write(pkt, 2) + await self._as_write(msg) + + # Can raise OSError if WiFi fails. Subclass traps. + async def subscribe(self, topic, qos): + pkt = bytearray(b"\x82\0\0\0") + pid = next(self.newpid) + self.rcv_pids.add(pid) + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, pid) + async with self.lock: + await self._as_write(pkt) + await self._send_str(topic) + await self._as_write(qos.to_bytes(1, "little")) + + if not await self._await_pid(pid): + raise OSError(-1) + + # Can raise OSError if WiFi fails. Subclass traps. + async def unsubscribe(self, topic): + pkt = bytearray(b"\xa2\0\0\0") + pid = next(self.newpid) + self.rcv_pids.add(pid) + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic), pid) + async with self.lock: + await self._as_write(pkt) + await self._send_str(topic) + + if not await self._await_pid(pid): + raise OSError(-1) + + # Wait for a single incoming MQTT message and process it. + # Subscribed messages are delivered to a callback previously + # set by .setup() method. Other (internal) MQTT + # messages processed internally. + # Immediate return if no data available. Called from ._handle_msg(). + async def wait_msg(self): + try: + res = self._sock.read(1) # Throws OSError on WiFi fail + except OSError as e: + if e.args[0] in BUSY_ERRORS: # Needed by RP2 + await asyncio.sleep_ms(0) + return + raise + if res is None: + return + if res == b"": + raise OSError(-1, "Empty response") + + if res == b"\xd0": # PINGRESP + await self._as_read(1) # Update .last_rx time + return + op = res[0] + + if op == 0x40: # PUBACK: save pid + sz = await self._as_read(1) + if sz != b"\x02": + raise OSError(-1, "Invalid PUBACK packet") + rcv_pid = await self._as_read(2) + pid = rcv_pid[0] << 8 | rcv_pid[1] + if pid in self.rcv_pids: + self.rcv_pids.discard(pid) + else: + raise OSError(-1, "Invalid pid in PUBACK packet") + + if op == 0x90: # SUBACK + resp = await self._as_read(4) + if resp[3] == 0x80: + raise OSError(-1, "Invalid SUBACK packet") + pid = resp[2] | (resp[1] << 8) + if pid in self.rcv_pids: + self.rcv_pids.discard(pid) + else: + raise OSError(-1, "Invalid pid in SUBACK packet") + + if op == 0xB0: # UNSUBACK + resp = await self._as_read(3) + pid = resp[2] | (resp[1] << 8) + if pid in self.rcv_pids: + self.rcv_pids.discard(pid) + else: + raise OSError(-1) + + if op & 0xF0 != 0x30: + return + sz = await self._recv_len() + topic_len = await self._as_read(2) + topic_len = (topic_len[0] << 8) | topic_len[1] + topic = await self._as_read(topic_len) + sz -= topic_len + 2 + if op & 6: + pid = await self._as_read(2) + pid = pid[0] << 8 | pid[1] + sz -= 2 + msg = await self._as_read(sz) + retained = op & 0x01 + if self._events: + self.queue.put(topic, msg, bool(retained)) + else: + self._cb(topic, msg, bool(retained)) + if op & 6 == 2: # qos 1 + pkt = bytearray(b"\x40\x02\0\0") # Send PUBACK + struct.pack_into("!H", pkt, 2, pid) + await self._as_write(pkt) + elif op & 6 == 4: # qos 2 not supported + raise OSError(-1, "QoS 2 not supported") + + +# MQTTClient class. Handles issues relating to connectivity. + + +class MQTTClient(MQTT_base): + def __init__(self, config): + super().__init__(config) + self._isconnected = False # Current connection state + keepalive = 1000 * self._keepalive # ms + self._ping_interval = keepalive // 4 if keepalive else 20000 + p_i = ( + config["ping_interval"] * 1000 + ) # Can specify shorter e.g. for subscribe-only + if p_i and p_i < self._ping_interval: + self._ping_interval = p_i + self._in_connect = False + self._has_connected = False # Define 'Clean Session' value to use. + self._tasks = [] + if ESP8266: + import esp + + esp.sleep_type( + 0 + ) # Improve connection integrity at cost of power consumption. + + async def wifi_connect(self, quick=False): + s = self._sta_if + if ESP8266: + if s.isconnected(): # 1st attempt, already connected. + return + s.active(True) + s.connect() # ESP8266 remembers connection. + for _ in range(60): + if ( + s.status() != network.STAT_CONNECTING + ): # Break out on fail or success. Check once per sec. + break + await asyncio.sleep(1) + if ( + s.status() == network.STAT_CONNECTING + ): # might hang forever awaiting dhcp lease renewal or something else + s.disconnect() + await asyncio.sleep(1) + if ( + not s.isconnected() + and self._ssid is not None + and self._wifi_pw is not None + ): + s.connect(self._ssid, self._wifi_pw) + while ( + s.status() == network.STAT_CONNECTING + ): # Break out on fail or success. Check once per sec. + await asyncio.sleep(1) + else: + s.active(True) + if RP2: # Disable auto-sleep. + # https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf + # para 3.6.3 + s.config(pm=0xA11140) + s.connect(self._ssid, self._wifi_pw) + for _ in range(60): # Break out on fail or success. Check once per sec. + await asyncio.sleep(1) + # Loop while connecting or no IP + if s.isconnected(): + break + if ESP32: + if s.status() != network.STAT_CONNECTING: # 1001 + break + elif PYBOARD: # No symbolic constants in network + if not 1 <= s.status() <= 2: + break + elif RP2: # 1 is STAT_CONNECTING. 2 reported by user (No IP?) + if not 1 <= s.status() <= 2: + break + else: # Timeout: still in connecting state + s.disconnect() + await asyncio.sleep(1) + + if not s.isconnected(): # Timed out + raise OSError("Wi-Fi connect timed out") + if not quick: # Skip on first connection only if power saving + # Ensure connection stays up for a few secs. + self.dprint("Checking WiFi integrity.") + for _ in range(5): + if not s.isconnected(): + raise OSError("Connection Unstable") # in 1st 5 secs + await asyncio.sleep(1) + self.dprint("Got reliable connection") + + async def connect( + self, *, quick=False + ): # Quick initial connect option for battery apps + if not self._has_connected: + await self.wifi_connect(quick) # On 1st call, caller handles error + # Note this blocks if DNS lookup occurs. Do it once to prevent + # blocking during later internet outage: + self._addr = socket.getaddrinfo(self.server, self.port)[0][-1] + self._in_connect = True # Disable low level ._isconnected check + try: + if not self._has_connected and self._clean_init and not self._clean: + # Power up. Clear previous session data but subsequently save it. + # Issue #40 + await self._connect(True) # Connect with clean session + try: + async with self.lock: + self._sock.write( + b"\xe0\0" + ) # Force disconnect but keep socket open + except OSError: + pass + self.dprint("Waiting for disconnect") + await asyncio.sleep(2) # Wait for broker to disconnect + self.dprint("About to reconnect with unclean session.") + await self._connect(self._clean) + except Exception: + self._close() + self._in_connect = False # Caller may run .isconnected() + raise + self.rcv_pids.clear() + # If we get here without error broker/LAN must be up. + self._isconnected = True + self._in_connect = False # Low level code can now check connectivity. + if not self._events: + asyncio.create_task(self._wifi_handler(True)) # User handler. + if not self._has_connected: + self._has_connected = True # Use normal clean flag on reconnect. + asyncio.create_task(self._keep_connected()) + # Runs forever unless user issues .disconnect() + + asyncio.create_task(self._handle_msg()) # Task quits on connection fail. + self._tasks.append(asyncio.create_task(self._keep_alive())) + if self.DEBUG: + self._tasks.append(asyncio.create_task(self._memory())) + if self._events: + self.up.set() # Connectivity is up + else: + asyncio.create_task(self._connect_handler(self)) # User handler. + + # Launched by .connect(). Runs until connectivity fails. Checks for and + # handles incoming messages. + async def _handle_msg(self): + try: + while self.isconnected(): + async with self.lock: + await self.wait_msg() # Immediate return if no message + await asyncio.sleep_ms(_DEFAULT_MS) # Let other tasks get lock + + except OSError: + pass + self._reconnect() # Broker or WiFi fail. + + # Keep broker alive MQTT spec 3.1.2.10 Keep Alive. + # Runs until ping failure or no response in keepalive period. + async def _keep_alive(self): + while self.isconnected(): + pings_due = ticks_diff(ticks_ms(), self.last_rx) // self._ping_interval + if pings_due >= 4: + self.dprint("Reconnect: broker fail.") + break + await asyncio.sleep_ms(self._ping_interval) + try: + await self._ping() + except OSError: + break + self._reconnect() # Broker or WiFi fail. + + async def _kill_tasks(self, kill_skt): # Cancel running tasks + for task in self._tasks: + task.cancel() + self._tasks.clear() + await asyncio.sleep_ms(0) # Ensure cancellation complete + if kill_skt: # Close socket + self._close() + + # DEBUG: show RAM messages. + async def _memory(self): + while True: + await asyncio.sleep(20) + gc.collect() + self.dprint("RAM free %d alloc %d", gc.mem_free(), gc.mem_alloc()) + + def isconnected(self): + if self._in_connect: # Disable low-level check during .connect() + return True + if self._isconnected and not self._sta_if.isconnected(): # It's going down. + self._reconnect() + return self._isconnected + + def _reconnect(self): # Schedule a reconnection if not underway. + if self._isconnected: + self._isconnected = False + asyncio.create_task(self._kill_tasks(True)) # Shut down tasks and socket + if self._events: # Signal an outage + self.down.set() + else: + asyncio.create_task(self._wifi_handler(False)) # User handler. + + # Await broker connection. + async def _connection(self): + while not self._isconnected: + await asyncio.sleep(1) + + # Scheduled on 1st successful connection. Runs forever maintaining wifi and + # broker connection. Must handle conditions at edge of WiFi range. + async def _keep_connected(self): + while self._has_connected: + if self.isconnected(): # Pause for 1 second + await asyncio.sleep(1) + gc.collect() + else: # Link is down, socket is closed, tasks are killed + try: + self._sta_if.disconnect() + except OSError: + self.dprint("Wi-Fi not started, unable to disconnect interface") + await asyncio.sleep(1) + try: + await self.wifi_connect() + except OSError: + continue + if ( + not self._has_connected + ): # User has issued the terminal .disconnect() + self.dprint("Disconnected, exiting _keep_connected") + break + try: + await self.connect() + # Now has set ._isconnected and scheduled _connect_handler(). + self.dprint("Reconnect OK!") + except OSError as e: + self.dprint("Error in reconnect. %s", e) + # Can get ECONNABORTED or -1. The latter signifies no or bad CONNACK received. + self._close() # Disconnect and try again. + self._in_connect = False + self._isconnected = False + self.dprint("Disconnected, exited _keep_connected") + + async def subscribe(self, topic, qos=0): + qos_check(qos) + while 1: + await self._connection() + try: + return await super().subscribe(topic, qos) + except OSError: + pass + self._reconnect() # Broker or WiFi fail. + + async def unsubscribe(self, topic): + while 1: + await self._connection() + try: + return await super().unsubscribe(topic) + except OSError: + pass + self._reconnect() # Broker or WiFi fail. + + async def publish(self, topic, msg, retain=False, qos=0): + qos_check(qos) + while 1: + await self._connection() + try: + return await super().publish(topic, msg, retain, qos) + except OSError: + pass + self._reconnect() # Broker or WiFi fail. diff --git a/scripts/lib/netman.py b/scripts/lib/netman.py new file mode 100644 index 0000000000000000000000000000000000000000..4c4e420ffba149719cd682c9efd246bada0ab4bd --- /dev/null +++ b/scripts/lib/netman.py @@ -0,0 +1,73 @@ +# .';:cc;. +# .,',;lol::c. +# ;';lddddlclo +# lcloxxoddodxdool:,. +# cxdddxdodxdkOkkkkkkkd:. +# .ldxkkOOOOkkOO000Okkxkkkkx:. +# .lddxkkOkOOO0OOO0000Okxxxxkkkk: +# 'ooddkkkxxkO0000KK00Okxdoodxkkkko +# .ooodxkkxxxOO000kkkO0KOxolooxkkxxkl +# lolodxkkxxkOx,. .lkdolodkkxxxO. +# doloodxkkkOk .... .,cxO; +# ddoodddxkkkk: ,oxxxkOdc'..o' +# :kdddxxxxd, ,lolccldxxxkkOOOkkkko, +# lOkxkkk; :xkkkkkkkkOOO000OOkkOOk. +# ;00Ok' 'O000OO0000000000OOOO0Od. +# .l0l.;OOO000000OOOOOO000000x, +# .'OKKKK00000000000000kc. +# .:ox0KKKKKKK0kdc,. +# ... +# +# Author: peppe8o +# Date: Jul 24th, 2022 +# Version: 1.0 +# https://peppe8o.com + +# modified by @sgbaird from source: +# https://peppe8o.com/getting-started-with-wifi-on-raspberry-pi-pico-w-and-micropython/ + +import time + +import network +import rp2 +from ubinascii import hexlify + + +def connectWiFi(ssid, password, country=None, wifi_energy_saver=False, retries=3): + for _ in range(retries): + try: + if country is not None: + # https://www.google.com/search?q=wifi+country+codes + rp2.country(country) + wlan = network.WLAN(network.STA_IF) + if not wifi_energy_saver: + wlan.config(pm=0xA11140) # avoid the energy-saving WiFi mode + wlan.active(True) + + mac = hexlify(network.WLAN().config("mac"), ":").decode() + print(f"MAC address: {mac}") + + wlan.connect(ssid, password) + # Wait for connect or fail + max_wait = 10 + while max_wait > 0: + if wlan.status() < 0 or wlan.status() >= 3: + break + max_wait -= 1 + print("waiting for connection...") + time.sleep(1) + + # Handle connection error + if wlan.status() != 3: + raise RuntimeError("network connection failed") + else: + print("connected") + status = wlan.ifconfig() + print("ip = " + status[0]) + return status + except RuntimeError as e: + print(f"Attempt failed with error: {e}. Retrying...") + raise RuntimeError( + "All attempts to connect to the network failed. Ensure you are using a 2.4 GHz WiFi network with WPA-2 authentication. See the additional prerequisites section from https://doi.org/10.1016/j.xpro.2023.102329 or the https://github.com/sparks-baird/self-driving-lab-demo/issues/76 for additional troubleshooting help." + ) + diff --git a/scripts/lib/sdcard/LICENSE b/scripts/lib/sdcard/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e3474e33dd81a6bc5c665361f026dd60c329db54 --- /dev/null +++ b/scripts/lib/sdcard/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013, 2014 Damien P. George + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/scripts/lib/sdcard/sdcard.py b/scripts/lib/sdcard/sdcard.py new file mode 100644 index 0000000000000000000000000000000000000000..2b6356b49a51b18882b87ddfa741e1cd476af566 --- /dev/null +++ b/scripts/lib/sdcard/sdcard.py @@ -0,0 +1,302 @@ +""" +MicroPython driver for SD cards using SPI bus. + +Requires an SPI bus and a CS pin. Provides readblocks and writeblocks +methods so the device can be mounted as a filesystem. + +Example usage on pyboard: + + import pyb, sdcard, os + sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5) + pyb.mount(sd, '/sd2') + os.listdir('/') + +Example usage on ESP8266: + + import machine, sdcard, os + sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15)) + os.mount(sd, '/sd') + os.listdir('/') + +Copied from source: https://raw.githubusercontent.com/micropython/micropython-lib/master/micropython/drivers/storage/sdcard/sdcard.py + +""" + +import time + +from micropython import const + +_CMD_TIMEOUT = const(100) + +_R1_IDLE_STATE = const(1 << 0) +# R1_ERASE_RESET = const(1 << 1) +_R1_ILLEGAL_COMMAND = const(1 << 2) +# R1_COM_CRC_ERROR = const(1 << 3) +# R1_ERASE_SEQUENCE_ERROR = const(1 << 4) +# R1_ADDRESS_ERROR = const(1 << 5) +# R1_PARAMETER_ERROR = const(1 << 6) +_TOKEN_CMD25 = const(0xFC) +_TOKEN_STOP_TRAN = const(0xFD) +_TOKEN_DATA = const(0xFE) + + +class SDCard: + def __init__(self, spi, cs, baudrate=1320000): + self.spi = spi + self.cs = cs + + self.cmdbuf = bytearray(6) + self.dummybuf = bytearray(512) + self.tokenbuf = bytearray(1) + for i in range(512): + self.dummybuf[i] = 0xFF + self.dummybuf_memoryview = memoryview(self.dummybuf) + + # initialise the card + self.init_card(baudrate) + + def init_spi(self, baudrate): + try: + master = self.spi.MASTER + except AttributeError: + # on ESP8266 + self.spi.init(baudrate=baudrate, phase=0, polarity=0) + else: + # on pyboard + self.spi.init(master, baudrate=baudrate, phase=0, polarity=0) + + def init_card(self, baudrate): + # init CS pin + self.cs.init(self.cs.OUT, value=1) + + # init SPI bus; use low data rate for initialisation + self.init_spi(100000) + + # clock card at least 100 cycles with cs high + for i in range(16): + self.spi.write(b"\xff") + + # CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts) + for _ in range(5): + if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE: + break + else: + raise OSError("no SD card") + + # CMD8: determine card version + r = self.cmd(8, 0x01AA, 0x87, 4) + if r == _R1_IDLE_STATE: + self.init_card_v2() + elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND): + self.init_card_v1() + else: + raise OSError("couldn't determine SD card version") + + # get the number of sectors + # CMD9: response R2 (R1 byte + 16-byte block read) + if self.cmd(9, 0, 0, 0, False) != 0: + raise OSError("no response from SD card") + csd = bytearray(16) + self.readinto(csd) + if csd[0] & 0xC0 == 0x40: # CSD version 2.0 + self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024 + elif csd[0] & 0xC0 == 0x00: # CSD version 1.0 (old, <=2GB) + c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6 + c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7 + read_bl_len = csd[5] & 0b1111 + capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len) + self.sectors = capacity // 512 + else: + raise OSError("SD card CSD format not supported") + # print('sectors', self.sectors) + + # CMD16: set block length to 512 bytes + if self.cmd(16, 512, 0) != 0: + raise OSError("can't set 512 block size") + + # set to high data rate now that it's initialised + self.init_spi(baudrate) + + def init_card_v1(self): + for i in range(_CMD_TIMEOUT): + time.sleep_ms(50) + self.cmd(55, 0, 0) + if self.cmd(41, 0, 0) == 0: + # SDSC card, uses byte addressing in read/write/erase commands + self.cdv = 512 + # print("[SDCard] v1 card") + return + raise OSError("timeout waiting for v1 card") + + def init_card_v2(self): + for i in range(_CMD_TIMEOUT): + time.sleep_ms(50) + self.cmd(58, 0, 0, 4) + self.cmd(55, 0, 0) + if self.cmd(41, 0x40000000, 0) == 0: + self.cmd( + 58, 0, 0, -4 + ) # 4-byte response, negative means keep the first byte + ocr = self.tokenbuf[0] # get first byte of response, which is OCR + if not ocr & 0x40: + # SDSC card, uses byte addressing in read/write/erase commands + self.cdv = 512 + else: + # SDHC/SDXC card, uses block addressing in read/write/erase commands + self.cdv = 1 + # print("[SDCard] v2 card") + return + raise OSError("timeout waiting for v2 card") + + def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False): + self.cs(0) + + # create and send the command + buf = self.cmdbuf + buf[0] = 0x40 | cmd + buf[1] = arg >> 24 + buf[2] = arg >> 16 + buf[3] = arg >> 8 + buf[4] = arg + buf[5] = crc + self.spi.write(buf) + + if skip1: + self.spi.readinto(self.tokenbuf, 0xFF) + + # wait for the response (response[7] == 0) + for i in range(_CMD_TIMEOUT): + self.spi.readinto(self.tokenbuf, 0xFF) + response = self.tokenbuf[0] + if not (response & 0x80): + # this could be a big-endian integer that we are getting here + # if final<0 then store the first byte to tokenbuf and discard the rest + if final < 0: + self.spi.readinto(self.tokenbuf, 0xFF) + final = -1 - final + for j in range(final): + self.spi.write(b"\xff") + if release: + self.cs(1) + self.spi.write(b"\xff") + return response + + # timeout + self.cs(1) + self.spi.write(b"\xff") + return -1 + + def readinto(self, buf): + self.cs(0) + + # read until start byte (0xff) + for i in range(_CMD_TIMEOUT): + self.spi.readinto(self.tokenbuf, 0xFF) + if self.tokenbuf[0] == _TOKEN_DATA: + break + time.sleep_ms(1) + else: + self.cs(1) + raise OSError("timeout waiting for response") + + # read data + mv = self.dummybuf_memoryview + if len(buf) != len(mv): + mv = mv[: len(buf)] + self.spi.write_readinto(mv, buf) + + # read checksum + self.spi.write(b"\xff") + self.spi.write(b"\xff") + + self.cs(1) + self.spi.write(b"\xff") + + def write(self, token, buf): + self.cs(0) + + # send: start of block, data, checksum + self.spi.read(1, token) + self.spi.write(buf) + self.spi.write(b"\xff") + self.spi.write(b"\xff") + + # check the response + if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05: + self.cs(1) + self.spi.write(b"\xff") + return + + # wait for write to finish + while self.spi.read(1, 0xFF)[0] == 0: + pass + + self.cs(1) + self.spi.write(b"\xff") + + def write_token(self, token): + self.cs(0) + self.spi.read(1, token) + self.spi.write(b"\xff") + # wait for write to finish + while self.spi.read(1, 0xFF)[0] == 0x00: + pass + + self.cs(1) + self.spi.write(b"\xff") + + def readblocks(self, block_num, buf): + nblocks = len(buf) // 512 + assert nblocks and not len(buf) % 512, "Buffer length is invalid" + if nblocks == 1: + # CMD17: set read address for single block + if self.cmd(17, block_num * self.cdv, 0, release=False) != 0: + # release the card + self.cs(1) + raise OSError(5) # EIO + # receive the data and release card + self.readinto(buf) + else: + # CMD18: set read address for multiple blocks + if self.cmd(18, block_num * self.cdv, 0, release=False) != 0: + # release the card + self.cs(1) + raise OSError(5) # EIO + offset = 0 + mv = memoryview(buf) + while nblocks: + # receive the data and release card + self.readinto(mv[offset : offset + 512]) + offset += 512 + nblocks -= 1 + if self.cmd(12, 0, 0xFF, skip1=True): + raise OSError(5) # EIO + + def writeblocks(self, block_num, buf): + nblocks, err = divmod(len(buf), 512) + assert nblocks and not err, "Buffer length is invalid" + if nblocks == 1: + # CMD24: set write address for single block + if self.cmd(24, block_num * self.cdv, 0) != 0: + raise OSError(5) # EIO + + # send the data + self.write(_TOKEN_DATA, buf) + else: + # CMD25: set write address for first block + if self.cmd(25, block_num * self.cdv, 0) != 0: + raise OSError(5) # EIO + # send the data + offset = 0 + mv = memoryview(buf) + while nblocks: + self.write(_TOKEN_CMD25, mv[offset : offset + 512]) + offset += 512 + nblocks -= 1 + self.write_token(_TOKEN_STOP_TRAN) + + def ioctl(self, op, arg): + if op == 4: # get number of blocks + return self.sectors + if op == 5: # get block size in bytes + return 512 diff --git a/scripts/lib/sdl_demo_utils.py b/scripts/lib/sdl_demo_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..009bbeed08bd19c9ef718d57a7ef7caa037f64f5 --- /dev/null +++ b/scripts/lib/sdl_demo_utils.py @@ -0,0 +1,276 @@ +import json +import sys +from time import localtime, sleep, ticks_diff, ticks_ms # type: ignore + +import uos +from data_logging import ( + get_local_timestamp, + get_onboard_temperature, + write_payload_backup, +) +from machine import PWM, Pin +from ufastrsa.genprime import genrsa +from ufastrsa.rsa import RSA +from uio import StringIO + + +def beep(buzzer, power=0.005): + buzzer.freq(300) + buzzer.duty_u16(round(65535 * power)) + sleep(0.15) + buzzer.duty_u16(0) + + +def get_traceback(err): + try: + with StringIO() as f: # type: ignore + sys.print_exception(err, f) + return f.getvalue() + except Exception as err2: + print(err2) + return f"Failed to extract file and line number due to {err2}.\nOriginal error: {err}" # noqa: E501 + + +def merge_two_dicts(x, y): + z = x.copy() # start with keys and values of x + z.update(y) # modifies z with keys and values of y + return z + + +def path_exists(path): + # Check if path exists. + # Works for relative and absolute path. + parent = "" # parent folder name + name = path # name of file/folder + + # Check if file/folder has a parent folder + index = path.rstrip("/").rfind("/") + if index >= 0: + index += 1 + parent = path[: index - 1] + name = path[index:] + + # Searching with iterator is more efficient if the parent contains lost of files/folders + # return name in uos.listdir(parent) + return any((name == x[0]) for x in uos.ilistdir(parent)) + + +def encrypt_id(my_id, verbose=False): + rsa_path = "rsa.json" + # if path_exists(rsa_path): + try: + with open(rsa_path, "r") as f: + cipher_data = json.load(f) + cipher = RSA( + cipher_data["bits"], + n=cipher_data["n"], + e=cipher_data["e"], + d=cipher_data["d"], + ) + except (KeyError, OSError) as e: + print(e) + print("Generating new RSA parameters...") + bits = 256 + bits, n, e, d = genrsa(bits, e=65537) # type: ignore + cipher = RSA(bits, n=n, e=e, d=d) + with open("rsa.json", "w") as f: + json.dump(dict(bits=bits, n=n, e=e, d=d), f) + + if verbose: + with open(rsa_path, "r") as f: + cipher_data = json.load(f) + print("RSA parameters (keep private):") + print(cipher_data) + + my_id = int.from_bytes(cipher.pkcs_encrypt(my_id), "big") + return my_id + + +def decrypt_id(my_id): + rsa_path = "rsa.json" + if path_exists(rsa_path): + with open(rsa_path, "r") as f: + cipher_data = json.load(f) + cipher = RSA( + cipher_data["bits"], + n=cipher_data["n"], + e=cipher_data["e"], + d=cipher_data["d"], + ) + else: + bits = 256 + bits, n, e, d = genrsa(bits, e=65537) # type: ignore + cipher = RSA(bits, n=n, e=e, d=d) + with open("rsa.json", "w") as f: + json.dump(dict(bits=bits, n=n, e=e, d=d), f) + + my_id = int.from_bytes(cipher.pkcs_decrypt(my_id), "big") + return my_id + + +def get_onboard_led(): + try: + onboard_led = Pin("LED", Pin.OUT) # only works for Pico W + except Exception as e: + print(e) + onboard_led = Pin(25, Pin.OUT) + return onboard_led + + +class Experiment(object): + def __init__( + self, + run_experiment_fn, + devices, + reset_experiment_fn=None, + validate_inputs_fn=None, + emergency_shutdown_fn=None, + buzzer=None, + sdcard_ready=False, + ) -> None: + self.validate_inputs_fn = validate_inputs_fn + self.run_experiment_fn = run_experiment_fn + self.reset_experiment_fn = reset_experiment_fn + self.devices = devices + self.emergency_shutdown_fn = emergency_shutdown_fn + self.buzzer = buzzer + self.sdcard_ready = sdcard_ready + + if self.reset_experiment_fn is None: + + def do_nothing(*args, **kwargs): + pass + + self.reset_experiment_fn = do_nothing + + if self.emergency_shutdown_fn is None: + self.emergency_shutdown_fn = self.reset_experiment_fn + + if self.validate_inputs_fn is None: + + def no_input_validation(*args, **kwargs): + return True + + self.validate_inputs_fn = no_input_validation + + if self.buzzer is None: + self.buzzer = PWM(Pin(18)) + + def try_experiment(self, msg): + payload_data = {} + # # pin numbers not used here, but can help with organization for complex tasks + # p = int(t[5:]) # pin number + + print(msg) + + # careful not to throw an unrecoverable error due to bad request + # Perform the experiment and record the results + try: + parameters = json.loads(msg) + payload_data["_input_message"] = parameters + + # don't allow access to hardware if any input values are out of bounds + self.validate_inputs_fn(parameters) # type: ignore + + beep(self.buzzer) + sensor_data = self.run_experiment_fn(parameters, self.devices) + payload_data = merge_two_dicts(payload_data, sensor_data) + + except Exception as err: + print(err) + if "_input_message" not in payload_data.keys(): + payload_data["_input_message"] = msg + payload_data["error"] = get_traceback(err) + + try: + payload_data["onboard_temperature_K"] = get_onboard_temperature(unit="K") + payload_data["sd_card_ready"] = self.sdcard_ready + stamp, time_str = get_local_timestamp(return_str=True) # type: ignore + payload_data["utc_timestamp"] = stamp + payload_data["utc_time_str"] = time_str + except OverflowError as e: + print(get_traceback(e)) + except Exception as e: + print(get_traceback(e)) + + try: + parameters = json.loads(msg) + self.reset_experiment_fn(parameters, devices=self.devices) # type: ignore + except Exception as e: + try: + self.emergency_shutdown_fn(devices=self.devices) # type: ignore + payload_data["reset_error"] = get_traceback(e) + except Exception as e: + payload_data["emergency_error"] = get_traceback(e) + + return payload_data + + def write_to_sd_card(self, payload_data, fpath="/sd/experiments.txt"): + try: + write_payload_backup(payload_data, fpath=fpath) + except Exception as e: + w = f"Failed to write to SD card: {get_traceback(e)}" + print(w) + payload_data["warning"] = w + + return payload_data + + # def log_to_mongodb( + # self, + # payload_data, + # api_key: str, + # url: str, + # cluster_name: str, + # database_name: str, + # collection_name: str, + # verbose: bool = True, + # retries: int = 2, + # ): + # try: + # log_to_mongodb( + # payload_data, + # url=url, + # api_key=api_key, + # cluster_name=cluster_name, + # database_name=database_name, + # collection_name=collection_name, + # verbose=verbose, + # retries=retries, + # ) + # except Exception as e: + # print(f"Failed to log to MongoDB backend: {get_traceback(e)}") + + +def heartbeat(client, first, ping_interval_ms=15000): + global lastping + if first: + client.ping() + lastping = ticks_ms() + if ticks_diff(ticks_ms(), lastping) >= ping_interval_ms: + client.ping() + lastping = ticks_ms() + return + + +def sign_of_life(led, first, blink_interval_ms=5000): + global last_blink + if first: + led.on() + last_blink = ticks_ms() + time_since = ticks_diff(ticks_ms(), last_blink) + if led.value() == 0 and time_since >= blink_interval_ms: + led.toggle() + last_blink = ticks_ms() + elif led.value() == 1 and time_since >= 500: + led.toggle() + last_blink = ticks_ms() + + +class DummyMotor: + def __init__(self): + pass + + +class DummySensor: + def __init__(self): + pass diff --git a/scripts/lib/smbus2-0.5.0.dist-info/METADATA b/scripts/lib/smbus2-0.5.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..2226ef2e4c95b340076abcf6e9485157f1a35865 --- /dev/null +++ b/scripts/lib/smbus2-0.5.0.dist-info/METADATA @@ -0,0 +1,234 @@ +Metadata-Version: 2.1 +Name: smbus2 +Version: 0.5.0 +Summary: smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python +Home-page: https://github.com/kplindegaard/smbus2 +Author: Karl-Petter Lindegaard +Author-email: kp.lindegaard@gmail.com +License: MIT +Keywords: smbus,smbus2,python,i2c,raspberrypi,linux +Classifier: Development Status :: 4 - Beta +Classifier: Topic :: Utilities +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Description-Content-Type: text/markdown +License-File: LICENSE +Provides-Extra: docs +Requires-Dist: sphinx>=1.5.3; extra == "docs" +Provides-Extra: qa +Requires-Dist: flake8; extra == "qa" + +# smbus2 +A drop-in replacement for smbus-cffi/smbus-python in pure Python + +[![Build Status](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml/badge.svg?branch=master)](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml) +[![Documentation Status](https://readthedocs.org/projects/smbus2/badge/?version=latest)](http://smbus2.readthedocs.io/en/latest/?badge=latest) +![CodeQL](https://github.com/kplindegaard/smbus2/actions/workflows/codeql-analysis.yml/badge.svg?branch=master) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=kplindegaard_smbus2&metric=alert_status)](https://sonarcloud.io/dashboard?id=kplindegaard_smbus2) + +![Python Verions](https://img.shields.io/pypi/pyversions/smbus2.svg) +[![PyPi Version](https://img.shields.io/pypi/v/smbus2.svg)](https://pypi.org/project/smbus2/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/smbus2)](https://pypi.org/project/smbus2/) + +# Introduction + +smbus2 is (yet another) pure Python implementation of the [python-smbus](http://www.lm-sensors.org/browser/i2c-tools/trunk/py-smbus/) package. + +It was designed from the ground up with two goals in mind: + +1. It should be a drop-in replacement of smbus. The syntax shall be the same. +2. Use the inherent i2c structs and unions to a greater extent than other pure Python implementations like [pysmbus](https://github.com/bjornt/pysmbus) does. By doing so, it will be more feature complete and easier to extend. + +Currently supported features are: + +* Get i2c capabilities (I2C_FUNCS) +* SMBus Packet Error Checking (PEC) support +* read_byte +* write_byte +* read_byte_data +* write_byte_data +* read_word_data +* write_word_data +* read_i2c_block_data +* write_i2c_block_data +* write_quick +* process_call +* read_block_data +* write_block_data +* block_process_call +* i2c_rdwr - *combined write/read transactions with repeated start* + +It is developed on Python 2.7 but works without any modifications in Python 3.X too. + +More information about updates and general changes are recorded in the [change log](https://github.com/kplindegaard/smbus2/blob/master/CHANGELOG.md). + +# SMBus code examples + +smbus2 installs next to smbus as the package, so it's not really a 100% replacement. You must change the module name. + +## Example 1a: Read a byte + +```python +from smbus2 import SMBus + +# Open i2c bus 1 and read one byte from address 80, offset 0 +bus = SMBus(1) +b = bus.read_byte_data(80, 0) +print(b) +bus.close() +``` + +## Example 1b: Read a byte using 'with' + +This is the very same example but safer to use since the smbus will be closed automatically when exiting the with block. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + b = bus.read_byte_data(80, 0) + print(b) +``` + +## Example 1c: Read a byte with PEC enabled + +Same example with Packet Error Checking enabled. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + bus.pec = 1 # Enable PEC + b = bus.read_byte_data(80, 0) + print(b) +``` + +## Example 2: Read a block of data + +You can read up to 32 bytes at once. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + # Read a block of 16 bytes from address 80, offset 0 + block = bus.read_i2c_block_data(80, 0, 16) + # Returned value is a list of 16 bytes + print(block) +``` + +## Example 3: Write a byte + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + # Write a byte to address 80, offset 0 + data = 45 + bus.write_byte_data(80, 0, data) +``` + +## Example 4: Write a block of data + +It is possible to write 32 bytes at the time, but I have found that error-prone. Write less and add a delay in between if you run into trouble. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + # Write a block of 8 bytes to address 80 from offset 0 + data = [1, 2, 3, 4, 5, 6, 7, 8] + bus.write_i2c_block_data(80, 0, data) +``` + +# I2C + +Starting with v0.2, the smbus2 library also has support for combined read and write transactions. *i2c_rdwr* is not really a SMBus feature but comes in handy when the master needs to: + +1. read or write bulks of data larger than SMBus' 32 bytes limit. +1. write some data and then read from the slave with a repeated start and no stop bit between. + +Each operation is represented by a *i2c_msg* message object. + + +## Example 5: Single i2c_rdwr + +```python +from smbus2 import SMBus, i2c_msg + +with SMBus(1) as bus: + # Read 64 bytes from address 80 + msg = i2c_msg.read(80, 64) + bus.i2c_rdwr(msg) + + # Write a single byte to address 80 + msg = i2c_msg.write(80, [65]) + bus.i2c_rdwr(msg) + + # Write some bytes to address 80 + msg = i2c_msg.write(80, [65, 66, 67, 68]) + bus.i2c_rdwr(msg) +``` + +## Example 6: Dual i2c_rdwr + +To perform dual operations just add more i2c_msg instances to the bus call: + +```python +from smbus2 import SMBus, i2c_msg + +# Single transaction writing two bytes then read two at address 80 +write = i2c_msg.write(80, [40, 50]) +read = i2c_msg.read(80, 2) +with SMBus(1) as bus: + bus.i2c_rdwr(write, read) +``` + +## Example 7: Access i2c_msg data + +All data is contained in the i2c_msg instances. Here are some data access alternatives. + +```python +# 1: Convert message content to list +msg = i2c_msg.write(60, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) +data = list(msg) # data = [1, 2, 3, ...] +print(len(data)) # => 10 + +# 2: i2c_msg is iterable +for value in msg: + print(value) + +# 3: Through i2c_msg properties +for k in range(msg.len): + print(msg.buf[k]) +``` + +# Installation instructions + +From [PyPi](https://pypi.org/) with `pip`: + +``` +pip install smbus2 +``` + +From [conda-forge](https://anaconda.org/conda-forge) using `conda`: + +``` +conda install -c conda-forge smbus2 +``` + +Installation from source code is straight forward: + +``` +python setup.py install +``` diff --git a/scripts/lib/smbus2-0.5.0.dist-info/RECORD b/scripts/lib/smbus2-0.5.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..7a53a38263f7ba8373ad2bf56faebf9db53a5a28 --- /dev/null +++ b/scripts/lib/smbus2-0.5.0.dist-info/RECORD @@ -0,0 +1,6 @@ +smbus2-0.5.0.dist-info/METADATA,, +smbus2/__init__.py,, +smbus2/py.typed,, +smbus2/smbus2.py,, +smbus2/smbus2.pyi,, +smbus2-0.5.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/lib/smbus2/__init__.py b/scripts/lib/smbus2/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f7f465eb9efe26bce34bee130240cd66a09a3bf6 --- /dev/null +++ b/scripts/lib/smbus2/__init__.py @@ -0,0 +1,26 @@ +"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python""" +# The MIT License (MIT) +# Copyright (c) 2020 Karl-Petter Lindegaard +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .smbus2 import SMBus, i2c_msg, I2cFunc # noqa: F401 + +__version__ = "0.5.0" +__all__ = ["SMBus", "i2c_msg", "I2cFunc"] diff --git a/scripts/lib/smbus2/py.typed b/scripts/lib/smbus2/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scripts/lib/smbus2/smbus2.py b/scripts/lib/smbus2/smbus2.py new file mode 100644 index 0000000000000000000000000000000000000000..a35868f50525cfc4fc7c35c58730322a5786bb6d --- /dev/null +++ b/scripts/lib/smbus2/smbus2.py @@ -0,0 +1,660 @@ +"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python""" +# The MIT License (MIT) +# Copyright (c) 2020 Karl-Petter Lindegaard +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import sys +from fcntl import ioctl +from ctypes import c_uint32, c_uint8, c_uint16, c_char, POINTER, Structure, Array, Union, create_string_buffer, string_at + + +# Commands from uapi/linux/i2c-dev.h +I2C_SLAVE = 0x0703 # Use this slave address +I2C_SLAVE_FORCE = 0x0706 # Use this slave address, even if it is already in use by a driver! +I2C_FUNCS = 0x0705 # Get the adapter functionality mask +I2C_RDWR = 0x0707 # Combined R/W transfer (one STOP only) +I2C_SMBUS = 0x0720 # SMBus transfer. Takes pointer to i2c_smbus_ioctl_data +I2C_PEC = 0x0708 # != 0 to use PEC with SMBus + +# SMBus transfer read or write markers from uapi/linux/i2c.h +I2C_SMBUS_WRITE = 0 +I2C_SMBUS_READ = 1 + +# Size identifiers uapi/linux/i2c.h +I2C_SMBUS_QUICK = 0 +I2C_SMBUS_BYTE = 1 +I2C_SMBUS_BYTE_DATA = 2 +I2C_SMBUS_WORD_DATA = 3 +I2C_SMBUS_PROC_CALL = 4 +I2C_SMBUS_BLOCK_DATA = 5 # This isn't supported by Pure-I2C drivers with SMBUS emulation, like those in RaspberryPi, OrangePi, etc :( +I2C_SMBUS_BLOCK_PROC_CALL = 7 # Like I2C_SMBUS_BLOCK_DATA, it isn't supported by Pure-I2C drivers either. +I2C_SMBUS_I2C_BLOCK_DATA = 8 +I2C_SMBUS_BLOCK_MAX = 32 + +# To determine what functionality is present (uapi/linux/i2c.h) +try: + from enum import IntFlag +except ImportError: + IntFlag = int + + +class I2cFunc(IntFlag): + """ + These flags identify the operations supported by an I2C/SMBus device. + + You can test these flags on your `smbus.funcs` + + On newer python versions, I2cFunc is an IntFlag enum, but it + falls back to class with a bunch of int constants on older releases. + """ + I2C = 0x00000001 + ADDR_10BIT = 0x00000002 + PROTOCOL_MANGLING = 0x00000004 # I2C_M_IGNORE_NAK etc. + SMBUS_PEC = 0x00000008 + NOSTART = 0x00000010 # I2C_M_NOSTART + SLAVE = 0x00000020 + SMBUS_BLOCK_PROC_CALL = 0x00008000 # SMBus 2.0 + SMBUS_QUICK = 0x00010000 + SMBUS_READ_BYTE = 0x00020000 + SMBUS_WRITE_BYTE = 0x00040000 + SMBUS_READ_BYTE_DATA = 0x00080000 + SMBUS_WRITE_BYTE_DATA = 0x00100000 + SMBUS_READ_WORD_DATA = 0x00200000 + SMBUS_WRITE_WORD_DATA = 0x00400000 + SMBUS_PROC_CALL = 0x00800000 + SMBUS_READ_BLOCK_DATA = 0x01000000 + SMBUS_WRITE_BLOCK_DATA = 0x02000000 + SMBUS_READ_I2C_BLOCK = 0x04000000 # I2C-like block xfer + SMBUS_WRITE_I2C_BLOCK = 0x08000000 # w/ 1-byte reg. addr. + SMBUS_HOST_NOTIFY = 0x10000000 + + SMBUS_BYTE = 0x00060000 + SMBUS_BYTE_DATA = 0x00180000 + SMBUS_WORD_DATA = 0x00600000 + SMBUS_BLOCK_DATA = 0x03000000 + SMBUS_I2C_BLOCK = 0x0c000000 + SMBUS_EMUL = 0x0eff0008 + + +# i2c_msg flags from uapi/linux/i2c.h +I2C_M_RD = 0x0001 + +# Pointer definitions +LP_c_uint8 = POINTER(c_uint8) +LP_c_uint16 = POINTER(c_uint16) +LP_c_uint32 = POINTER(c_uint32) + + +############################################################# +# Type definitions as in i2c.h + + +class i2c_smbus_data(Array): + """ + Adaptation of the i2c_smbus_data union in ``i2c.h``. + + Data for SMBus messages. + """ + _length_ = I2C_SMBUS_BLOCK_MAX + 2 + _type_ = c_uint8 + + +class union_i2c_smbus_data(Union): + _fields_ = [ + ("byte", c_uint8), + ("word", c_uint16), + ("block", i2c_smbus_data) + ] + + +union_pointer_type = POINTER(union_i2c_smbus_data) + + +class i2c_smbus_ioctl_data(Structure): + """ + As defined in ``i2c-dev.h``. + """ + _fields_ = [ + ('read_write', c_uint8), + ('command', c_uint8), + ('size', c_uint32), + ('data', union_pointer_type)] + __slots__ = [name for name, type in _fields_] + + @staticmethod + def create(read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE_DATA): + u = union_i2c_smbus_data() + return i2c_smbus_ioctl_data( + read_write=read_write, command=command, size=size, + data=union_pointer_type(u)) + + +############################################################# +# Type definitions for i2c_rdwr combined transactions + + +class i2c_msg(Structure): + """ + As defined in ``i2c.h``. + """ + _fields_ = [ + ('addr', c_uint16), + ('flags', c_uint16), + ('len', c_uint16), + ('buf', POINTER(c_char))] + + def __iter__(self): + """ Iterator / Generator + + :return: iterates over :py:attr:`buf` + :rtype: :py:class:`generator` which returns int values + """ + idx = 0 + while idx < self.len: + yield ord(self.buf[idx]) + idx += 1 + + def __len__(self): + return self.len + + def __bytes__(self): + return string_at(self.buf, self.len) + + def __repr__(self): + return 'i2c_msg(%d,%d,%r)' % (self.addr, self.flags, self.__bytes__()) + + def __str__(self): + s = self.__bytes__() + # Throw away non-decodable bytes + s = s.decode(errors="ignore") + return s + + @staticmethod + def read(address, length): + """ + Prepares an i2c read transaction. + + :param address: Slave address. + :type address: int + :param length: Number of bytes to read. + :type length: int + :return: New :py:class:`i2c_msg` instance for read operation. + :rtype: :py:class:`i2c_msg` + """ + arr = create_string_buffer(length) + return i2c_msg( + addr=address, flags=I2C_M_RD, len=length, + buf=arr) + + @staticmethod + def write(address, buf): + """ + Prepares an i2c write transaction. + + :param address: Slave address. + :type address: int + :param buf: Bytes to write. Either list of values or str. + :type buf: list + :return: New :py:class:`i2c_msg` instance for write operation. + :rtype: :py:class:`i2c_msg` + """ + if sys.version_info.major >= 3: + if type(buf) is str: + buf = bytes(map(ord, buf)) + else: + buf = bytes(buf) + else: + if type(buf) is not str: + buf = ''.join([chr(x) for x in buf]) + arr = create_string_buffer(buf, len(buf)) + return i2c_msg( + addr=address, flags=0, len=len(arr), + buf=arr) + + +class i2c_rdwr_ioctl_data(Structure): + """ + As defined in ``i2c-dev.h``. + """ + _fields_ = [ + ('msgs', POINTER(i2c_msg)), + ('nmsgs', c_uint32) + ] + __slots__ = [name for name, type in _fields_] + + @staticmethod + def create(*i2c_msg_instances): + """ + Factory method for creating a i2c_rdwr_ioctl_data struct that can + be called with ``ioctl(fd, I2C_RDWR, data)``. + + :param i2c_msg_instances: Up to 42 i2c_msg instances + :rtype: i2c_rdwr_ioctl_data + """ + n_msg = len(i2c_msg_instances) + msg_array = (i2c_msg * n_msg)(*i2c_msg_instances) + return i2c_rdwr_ioctl_data( + msgs=msg_array, + nmsgs=n_msg + ) + + +############################################################# + + +class SMBus(object): + + def __init__(self, bus=None, force=False): + """ + Initialize and (optionally) open an i2c bus connection. + + :param bus: i2c bus number (e.g. 0 or 1) + or an absolute file path (e.g. `/dev/i2c-42`). + If not given, a subsequent call to ``open()`` is required. + :type bus: int or str + :param force: force using the slave address even when driver is + already using it. + :type force: boolean + """ + self.fd = None + self.funcs = I2cFunc(0) + if bus is not None: + self.open(bus) + self.address = None + self.force = force + self._force_last = None + self._pec = 0 + + def __enter__(self): + """Enter handler.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit handler.""" + self.close() + + def open(self, bus): + """ + Open a given i2c bus. + + :param bus: i2c bus number (e.g. 0 or 1) + or an absolute file path (e.g. '/dev/i2c-42'). + :type bus: int or str + :raise TypeError: if type(bus) is not in (int, str) + """ + if isinstance(bus, int): + filepath = "/dev/i2c-{}".format(bus) + elif isinstance(bus, str): + filepath = bus + else: + raise TypeError("Unexpected type(bus)={}".format(type(bus))) + + self.fd = os.open(filepath, os.O_RDWR) + self.funcs = self._get_funcs() + + def close(self): + """ + Close the i2c connection. + """ + if self.fd: + os.close(self.fd) + self.fd = None + self._pec = 0 + self.address = None + self._force_last = None + + def _get_pec(self): + return self._pec + + def enable_pec(self, enable=True): + """ + Enable/Disable PEC (Packet Error Checking) - SMBus 1.1 and later + + :param enable: + :type enable: Boolean + """ + if not (self.funcs & I2cFunc.SMBUS_PEC): + raise IOError('SMBUS_PEC is not a feature') + self._pec = int(enable) + ioctl(self.fd, I2C_PEC, self._pec) + + pec = property(_get_pec, enable_pec) # Drop-in replacement for smbus member "pec" + """Get and set SMBus PEC. 0 = disabled (default), 1 = enabled.""" + + def _set_address(self, address, force=None): + """ + Set i2c slave address to use for subsequent calls. + + :param address: + :type address: int + :param force: + :type force: Boolean + """ + force = force if force is not None else self.force + if self.address != address or self._force_last != force: + if force is True: + ioctl(self.fd, I2C_SLAVE_FORCE, address) + else: + ioctl(self.fd, I2C_SLAVE, address) + self.address = address + self._force_last = force + + def _get_funcs(self): + """ + Returns a 32-bit value stating supported I2C functions. + + :rtype: int + """ + f = c_uint32() + ioctl(self.fd, I2C_FUNCS, f) + return f.value + + def write_quick(self, i2c_addr, force=None): + """ + Perform quick transaction. Throws IOError if unsuccessful. + :param i2c_addr: i2c address + :type i2c_addr: int + :param force: + :type force: Boolean + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=0, size=I2C_SMBUS_QUICK) + ioctl(self.fd, I2C_SMBUS, msg) + + def read_byte(self, i2c_addr, force=None): + """ + Read a single byte from a device. + + :rtype: int + :param i2c_addr: i2c address + :type i2c_addr: int + :param force: + :type force: Boolean + :return: Read byte value + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE + ) + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.byte + + def write_byte(self, i2c_addr, value, force=None): + """ + Write a single byte to a device. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param value: value to write + :type value: int + :param force: + :type force: Boolean + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=value, size=I2C_SMBUS_BYTE + ) + ioctl(self.fd, I2C_SMBUS, msg) + + def read_byte_data(self, i2c_addr, register, force=None): + """ + Read a single byte from a designated register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read + :type register: int + :param force: + :type force: Boolean + :return: Read byte value + :rtype: int + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BYTE_DATA + ) + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.byte + + def write_byte_data(self, i2c_addr, register, value, force=None): + """ + Write a byte to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to write to + :type register: int + :param value: Byte value to transmit + :type value: int + :param force: + :type force: Boolean + :rtype: None + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BYTE_DATA + ) + msg.data.contents.byte = value + ioctl(self.fd, I2C_SMBUS, msg) + + def read_word_data(self, i2c_addr, register, force=None): + """ + Read a single word (2 bytes) from a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read + :type register: int + :param force: + :type force: Boolean + :return: 2-byte word + :rtype: int + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_WORD_DATA + ) + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.word + + def write_word_data(self, i2c_addr, register, value, force=None): + """ + Write a single word (2 bytes) to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to write to + :type register: int + :param value: Word value to transmit + :type value: int + :param force: + :type force: Boolean + :rtype: None + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_WORD_DATA + ) + msg.data.contents.word = value + ioctl(self.fd, I2C_SMBUS, msg) + + def process_call(self, i2c_addr, register, value, force=None): + """ + Executes a SMBus Process Call, sending a 16-bit value and receiving a 16-bit response + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read/write to + :type register: int + :param value: Word value to transmit + :type value: int + :param force: + :type force: Boolean + :rtype: int + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_PROC_CALL + ) + msg.data.contents.word = value + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.word + + def read_block_data(self, i2c_addr, register, force=None): + """ + Read a block of up to 32-bytes from a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param force: + :type force: Boolean + :return: List of bytes + :rtype: list + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BLOCK_DATA + ) + ioctl(self.fd, I2C_SMBUS, msg) + length = msg.data.contents.block[0] + return msg.data.contents.block[1:length + 1] + + def write_block_data(self, i2c_addr, register, data, force=None): + """ + Write a block of byte data to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param data: List of bytes + :type data: list + :param force: + :type force: Boolean + :rtype: None + """ + length = len(data) + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_DATA + ) + msg.data.contents.block[0] = length + msg.data.contents.block[1:length + 1] = data + ioctl(self.fd, I2C_SMBUS, msg) + + def block_process_call(self, i2c_addr, register, data, force=None): + """ + Executes a SMBus Block Process Call, sending a variable-size data + block and receiving another variable-size response + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read/write to + :type register: int + :param data: List of bytes + :type data: list + :param force: + :type force: Boolean + :return: List of bytes + :rtype: list + """ + length = len(data) + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_PROC_CALL + ) + msg.data.contents.block[0] = length + msg.data.contents.block[1:length + 1] = data + ioctl(self.fd, I2C_SMBUS, msg) + length = msg.data.contents.block[0] + return msg.data.contents.block[1:length + 1] + + def read_i2c_block_data(self, i2c_addr, register, length, force=None): + """ + Read a block of byte data from a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param length: Desired block length + :type length: int + :param force: + :type force: Boolean + :return: List of bytes + :rtype: list + """ + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Desired block length over %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA + ) + msg.data.contents.byte = length + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.block[1:length + 1] + + def write_i2c_block_data(self, i2c_addr, register, data, force=None): + """ + Write a block of byte data to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param data: List of bytes + :type data: list + :param force: + :type force: Boolean + :rtype: None + """ + length = len(data) + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA + ) + msg.data.contents.block[0] = length + msg.data.contents.block[1:length + 1] = data + ioctl(self.fd, I2C_SMBUS, msg) + + def i2c_rdwr(self, *i2c_msgs): + """ + Combine a series of i2c read and write operations in a single + transaction (with repeated start bits but no stop bits in between). + + This method takes i2c_msg instances as input, which must be created + first with :py:meth:`i2c_msg.read` or :py:meth:`i2c_msg.write`. + + :param i2c_msgs: One or more i2c_msg class instances. + :type i2c_msgs: i2c_msg + :rtype: None + """ + ioctl_data = i2c_rdwr_ioctl_data.create(*i2c_msgs) + ioctl(self.fd, I2C_RDWR, ioctl_data) diff --git a/scripts/lib/smbus2/smbus2.pyi b/scripts/lib/smbus2/smbus2.pyi new file mode 100644 index 0000000000000000000000000000000000000000..0861e5387490e1c6364cc4d1ef836463960824a7 --- /dev/null +++ b/scripts/lib/smbus2/smbus2.pyi @@ -0,0 +1,148 @@ +from enum import IntFlag +from typing import Optional, Sequence, List, Type, SupportsBytes, Iterable +from typing import Union as _UnionT +from types import TracebackType +from ctypes import c_uint32, c_uint8, c_uint16, pointer, Structure, Array, Union + +I2C_SLAVE: int +I2C_SLAVE_FORCE: int +I2C_FUNCS: int +I2C_RDWR: int +I2C_SMBUS: int +I2C_PEC: int +I2C_SMBUS_WRITE: int +I2C_SMBUS_READ: int +I2C_SMBUS_QUICK: int +I2C_SMBUS_BYTE: int +I2C_SMBUS_BYTE_DATA: int +I2C_SMBUS_WORD_DATA: int +I2C_SMBUS_PROC_CALL: int +I2C_SMBUS_BLOCK_DATA: int +I2C_SMBUS_BLOCK_PROC_CALL: int +I2C_SMBUS_I2C_BLOCK_DATA: int +I2C_SMBUS_BLOCK_MAX: int + +class I2cFunc(IntFlag): + I2C = ... + ADDR_10BIT = ... + PROTOCOL_MANGLING = ... + SMBUS_PEC = ... + NOSTART = ... + SLAVE = ... + SMBUS_BLOCK_PROC_CALL = ... + SMBUS_QUICK = ... + SMBUS_READ_BYTE = ... + SMBUS_WRITE_BYTE = ... + SMBUS_READ_BYTE_DATA = ... + SMBUS_WRITE_BYTE_DATA = ... + SMBUS_READ_WORD_DATA = ... + SMBUS_WRITE_WORD_DATA = ... + SMBUS_PROC_CALL = ... + SMBUS_READ_BLOCK_DATA = ... + SMBUS_WRITE_BLOCK_DATA = ... + SMBUS_READ_I2C_BLOCK = ... + SMBUS_WRITE_I2C_BLOCK = ... + SMBUS_HOST_NOTIFY = ... + SMBUS_BYTE = ... + SMBUS_BYTE_DATA = ... + SMBUS_WORD_DATA = ... + SMBUS_BLOCK_DATA = ... + SMBUS_I2C_BLOCK = ... + SMBUS_EMUL = ... + +I2C_M_RD: int +LP_c_uint8: Type[pointer[c_uint8]] +LP_c_uint16: Type[pointer[c_uint16]] +LP_c_uint32: Type[pointer[c_uint32]] + +class i2c_smbus_data(Array): ... +class union_i2c_smbus_data(Union): ... + +union_pointer_type: pointer[union_i2c_smbus_data] + +class i2c_smbus_ioctl_data(Structure): + @staticmethod + def create( + read_write: int = ..., command: int = ..., size: int = ... + ) -> "i2c_smbus_ioctl_data": ... + +class i2c_msg(Structure): + def __iter__(self) -> int: ... + def __len__(self) -> int: ... + def __bytes__(self) -> str: ... + @staticmethod + def read(address: int, length: int) -> "i2c_msg": ... + @staticmethod + def write(address: int, buf: _UnionT[str, Iterable[int], SupportsBytes]) -> "i2c_msg": ... + +class i2c_rdwr_ioctl_data(Structure): + @staticmethod + def create(*i2c_msg_instances: Sequence[i2c_msg]) -> "i2c_rdwr_ioctl_data": ... + +class SMBus: + fd: Optional[int] = ... + funcs: I2cFunc = ... + address: Optional[int] = ... + force: bool = ... + pec: int = ... + def __init__( + self, bus: _UnionT[None, int, str] = ..., force: bool = ... + ) -> None: ... + def __enter__(self) -> "SMBus": ... + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: ... + def open(self, bus: _UnionT[int, str]) -> None: ... + def close(self) -> None: ... + def enable_pec(self, enable: bool = ...) -> None: ... + def write_quick(self, i2c_addr: int, force: Optional[bool] = ...) -> None: ... + def read_byte(self, i2c_addr: int, force: Optional[bool] = ...) -> int: ... + def write_byte( + self, i2c_addr: int, value: int, force: Optional[bool] = ... + ) -> None: ... + def read_byte_data( + self, i2c_addr: int, register: int, force: Optional[bool] = ... + ) -> int: ... + def write_byte_data( + self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... + ) -> None: ... + def read_word_data( + self, i2c_addr: int, register: int, force: Optional[bool] = ... + ) -> int: ... + def write_word_data( + self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... + ) -> None: ... + def process_call( + self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... + ): ... + def read_block_data( + self, i2c_addr: int, register: int, force: Optional[bool] = ... + ) -> List[int]: ... + def write_block_data( + self, + i2c_addr: int, + register: int, + data: Sequence[int], + force: Optional[bool] = ..., + ) -> None: ... + def block_process_call( + self, + i2c_addr: int, + register: int, + data: Sequence[int], + force: Optional[bool] = ..., + ) -> List[int]: ... + def read_i2c_block_data( + self, i2c_addr: int, register: int, length: int, force: Optional[bool] = ... + ) -> List[int]: ... + def write_i2c_block_data( + self, + i2c_addr: int, + register: int, + data: Sequence[int], + force: Optional[bool] = ..., + ) -> None: ... + def i2c_rdwr(self, *i2c_msgs: i2c_msg) -> None: ... diff --git a/scripts/lib/ufastrsa/__init__.py b/scripts/lib/ufastrsa/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scripts/lib/ufastrsa/genprime.py b/scripts/lib/ufastrsa/genprime.py new file mode 100644 index 0000000000000000000000000000000000000000..4478394bbcfef648250cbe960144cbd961013e12 --- /dev/null +++ b/scripts/lib/ufastrsa/genprime.py @@ -0,0 +1,136 @@ +from ufastrsa import srandom + +try: + from _crypto import NUMBER as tomsfastmath + + pow3_ = tomsfastmath.exptmod + invmod_ = tomsfastmath.invmod + generate_prime_ = tomsfastmath.generate_prime + + def genprime(num=1024, test=25, safe=False): + return generate_prime_(num, test, safe) + +except ImportError: + pow3_ = pow + + def invmod_(a, b): + c, d, e, f, g = 1, 0, 0, 1, b + while b: + q = a // b + a, c, d, b, e, f = b, e, f, a - q * b, c - q * e, d - q * f + assert a >= 0 and c % g >= 0 + return a == 1 and c % g or 0 + + def miller_rabin_pass(a, n): + n_minus_one = n - 1 + s, d = get_lowest_set_bit(n_minus_one) + a_to_power = pow3(a, d, n) + if a_to_power == 1: + return True + for i in range(s): + if a_to_power == n_minus_one: + return True + a_to_power = pow3(a_to_power, 2, n) + if a_to_power == n_minus_one: + return True + return False + + class MillerRabinTest: + def __init__(self, randint, repeat): + self.randint = randint + self.repeat = repeat + + def __call__(self, n): + randint = self.randint + n_minus_one = n - 1 + for repeat in range(self.repeat): + a = randint(1, n_minus_one) + if not miller_rabin_pass(a, n): + return False + return True + + class GenPrime: + def __init__(self, getrandbits, testfn): + self.getrandbits = getrandbits + self.testfn = testfn + + def __call__(self, bits): + getrandbits = self.getrandbits + testfn = self.testfn + while True: + p = (1 << (bits - 1)) | getrandbits(bits - 1) | 1 + if p % 3 != 0 and p % 5 != 0 and p % 7 != 0 and testfn(p): + break + return p + + miller_rabin_test = MillerRabinTest(srandom.randint, 25) + genprime = GenPrime(srandom.getrandbits, miller_rabin_test) + + +def pow3(x, y, z): + return pow3_(x, y, z) + + +def invmod(a, b): + return invmod_(a, b) + + +def get_lowest_set_bit(n): + i = 0 + while n: + if n & 1: + return i, n + n >>= 1 + i += 1 + raise "Error" + + +def gcd(a, b): + while b: + a, b = b, a % b + return a + + +def get_bit_length(n): + return srandom.get_bit_length(n) + + +class GenRSA: + def __init__(self, genprime): + self.genprime = genprime + + def __call__(self, bits, e=None, with_crt=False): + pbits = (bits + 1) >> 1 + qbits = bits - pbits + if e is None: + e = 65537 + elif e < 0: + e = self.genprime(-e) + while True: + p = self.genprime(pbits) + if gcd(e, p - 1) == 1: + break + while True: + while True: + q = self.genprime(qbits) + if gcd(e, q - 1) == 1 and p != q: + break + n = p * q + if get_bit_length(n) == bits: + break + p = max(p, q) + p_minus_1 = p - 1 + q_minus_1 = q - 1 + phi = p_minus_1 * q_minus_1 + d = invmod(e, phi) + if with_crt: + dp = d % p_minus_1 + dq = d % q_minus_1 + qinv = invmod(q, p) + assert qinv < p + return bits, n, e, d, p, q, dp, dq, qinv + else: + return bits, n, e, d + + +genrsa = GenRSA(genprime) diff --git a/scripts/lib/ufastrsa/rsa.py b/scripts/lib/ufastrsa/rsa.py new file mode 100644 index 0000000000000000000000000000000000000000..14fd2f833fd121a994cbac8104f1fe8049857fe6 --- /dev/null +++ b/scripts/lib/ufastrsa/rsa.py @@ -0,0 +1,46 @@ +from ufastrsa.genprime import pow3 +from ufastrsa.srandom import rndsrcnz + + +class RSA: + def __init__(self, bits, n=None, e=None, d=None): + self.bits = bits + self.bytes = (bits + 7) >> 3 + self.n = n + self.e = e + self.d = d + self.rndsrcnz = rndsrcnz + + def pkcs_sign(self, value): + len_padding = self.bytes - 3 - len(value) + assert len_padding >= 0, len_padding + base = int.from_bytes( + b"\x00\x01" + len_padding * b"\xff" + b"\x00" + value, "big" + ) + return int.to_bytes(pow3(base, self.d, self.n), self.bytes, "big") + + def pkcs_verify(self, value): + assert len(value) == self.bytes + signed = int.to_bytes( + pow3(int.from_bytes(value, "big"), self.e, self.n), self.bytes, "big" + ) + idx = signed.find(b"\0", 1) + assert idx != -1 and signed[:idx] == b"\x00\x01" + (idx - 2) * b"\xff" + return signed[idx + 1 :] + + def pkcs_encrypt(self, value): + len_padding = self.bytes - 3 - len(value) + assert len_padding >= 0 + base = int.from_bytes( + b"\x00\x02" + self.rndsrcnz(len_padding) + b"\x00" + value, "big" + ) + return int.to_bytes(pow3(base, self.e, self.n), self.bytes, "big") + + def pkcs_decrypt(self, value): + assert len(value) == self.bytes + decrypted = int.to_bytes( + pow3(int.from_bytes(value, "big"), self.d, self.n), self.bytes, "big" + ) + idx = decrypted.find(b"\0", 2) + assert idx != -1 and decrypted[:2] == b"\x00\x02" + return decrypted[idx + 1 :] diff --git a/scripts/lib/ufastrsa/srandom.py b/scripts/lib/ufastrsa/srandom.py new file mode 100644 index 0000000000000000000000000000000000000000..30bbc66699771fe5788c2a6b222a2b4dada901f2 --- /dev/null +++ b/scripts/lib/ufastrsa/srandom.py @@ -0,0 +1,47 @@ +from functools import reduce +from os import urandom + +from ufastrsa.util import get_bit_length + + +class Random: + def __init__(self, seed=None, rndsrc=None): + if rndsrc is None: + rndsrc = urandom + self.rndsrc = rndsrc + + def getrandbits(self, k): + if not k >= 0: + raise ValueError("number of bits must be >= 0") + return reduce( + lambda x, y: x << 8 | y, + self.rndsrc(k >> 3), + int.from_bytes(self.rndsrc(1), "little") & ((1 << (k & 7)) - 1), + ) + + def randint(self, a, b): + if a > b: + raise ValueError("empty range for randint(): %d, %d" % (a, b)) + c = 1 + b - a + k = get_bit_length(c - 1) + while True: + r = self.getrandbits(k) + if r <= c: + break + return a + r + + def rndsrcnz(self, size): + rv = self.rndsrc(size).replace(b"\x00", b"") + mv = size - len(rv) + while mv > 0: + rv += self.rndsrc(mv).replace(b"\x00", b"") + mv = size - len(rv) + assert len(rv) == size + return rv + + +basernd = Random() +rndsrc = basernd.rndsrc +getrandbits = basernd.getrandbits +randint = basernd.randint +rndsrcnz = basernd.rndsrcnz diff --git a/scripts/lib/ufastrsa/util.py b/scripts/lib/ufastrsa/util.py new file mode 100644 index 0000000000000000000000000000000000000000..f74b3d0ac09c60844f949f4ae0c142fe02195289 --- /dev/null +++ b/scripts/lib/ufastrsa/util.py @@ -0,0 +1,14 @@ +try: + int.bit_length(0) + + def get_bit_length(n): + return n.bit_length() + +except: + # Work around + def get_bit_length(n): + i = 0 + while n: + n >>= 1 + i += 1 + return i diff --git a/scripts/lib/umqtt/robust.py b/scripts/lib/umqtt/robust.py new file mode 100644 index 0000000000000000000000000000000000000000..2a2b56290d6e4638aad36086e8f5722a94c07b1d --- /dev/null +++ b/scripts/lib/umqtt/robust.py @@ -0,0 +1,44 @@ +import utime + +from . import simple + + +class MQTTClient(simple.MQTTClient): + DELAY = 2 + DEBUG = False + + def delay(self, i): + utime.sleep(self.DELAY) + + def log(self, in_reconnect, e): + if self.DEBUG: + if in_reconnect: + print("mqtt reconnect: %r" % e) + else: + print("mqtt: %r" % e) + + def reconnect(self): + i = 0 + while 1: + try: + return super().connect(False) + except OSError as e: + self.log(True, e) + i += 1 + self.delay(i) + + def publish(self, topic, msg, retain=False, qos=0): + while 1: + try: + return super().publish(topic, msg, retain, qos) + except OSError as e: + self.log(False, e) + self.reconnect() + + def wait_msg(self): + while 1: + try: + return super().wait_msg() + except OSError as e: + self.log(False, e) + self.reconnect() diff --git a/scripts/lib/umqtt/simple.py b/scripts/lib/umqtt/simple.py new file mode 100644 index 0000000000000000000000000000000000000000..5d09230c5da80ade8ffb61b2b5c5ed9c8e9df38a --- /dev/null +++ b/scripts/lib/umqtt/simple.py @@ -0,0 +1,217 @@ +import usocket as socket +import ustruct as struct + + +class MQTTException(Exception): + pass + + +class MQTTClient: + def __init__( + self, + client_id, + server, + port=0, + user=None, + password=None, + keepalive=0, + ssl=False, + ssl_params={}, + ): + if port == 0: + port = 8883 if ssl else 1883 + self.client_id = client_id + self.sock = None + self.server = server + self.port = port + self.ssl = ssl + self.ssl_params = ssl_params + self.pid = 0 + self.cb = None + self.user = user + self.pswd = password + self.keepalive = keepalive + self.lw_topic = None + self.lw_msg = None + self.lw_qos = 0 + self.lw_retain = False + + def _send_str(self, s): + self.sock.write(struct.pack("!H", len(s))) + self.sock.write(s) + + def _recv_len(self): + n = 0 + sh = 0 + while 1: + b = self.sock.read(1)[0] + n |= (b & 0x7F) << sh + if not b & 0x80: + return n + sh += 7 + + def set_callback(self, f): + self.cb = f + + def set_last_will(self, topic, msg, retain=False, qos=0): + assert 0 <= qos <= 2 + assert topic + self.lw_topic = topic + self.lw_msg = msg + self.lw_qos = qos + self.lw_retain = retain + + def connect(self, clean_session=True): + self.sock = socket.socket() + addr = socket.getaddrinfo(self.server, self.port)[0][-1] + self.sock.connect(addr) + if self.ssl: + # replaced ussl with ssl due to deprecation in MicroPython 1.23.0 + # (not PR'd on source repo, but I'm using mqtt_as in my workflows + # instead, anyway) + import ssl + + self.sock = ssl.wrap_socket(self.sock, **self.ssl_params) + premsg = bytearray(b"\x10\0\0\0\0\0") + msg = bytearray(b"\x04MQTT\x04\x02\0\0") + + sz = 10 + 2 + len(self.client_id) + msg[6] = clean_session << 1 + if self.user is not None: + sz += 2 + len(self.user) + 2 + len(self.pswd) + msg[6] |= 0xC0 + if self.keepalive: + assert self.keepalive < 65536 + msg[7] |= self.keepalive >> 8 + msg[8] |= self.keepalive & 0x00FF + if self.lw_topic: + sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg) + msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3 + msg[6] |= self.lw_retain << 5 + + i = 1 + while sz > 0x7F: + premsg[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + premsg[i] = sz + + self.sock.write(premsg, i + 2) + self.sock.write(msg) + # print(hex(len(msg)), hexlify(msg, ":")) + self._send_str(self.client_id) + if self.lw_topic: + self._send_str(self.lw_topic) + self._send_str(self.lw_msg) + if self.user is not None: + self._send_str(self.user) + self._send_str(self.pswd) + resp = self.sock.read(4) + assert resp[0] == 0x20 and resp[1] == 0x02 + if resp[3] != 0: + raise MQTTException(resp[3]) + return resp[2] & 1 + + def disconnect(self): + self.sock.write(b"\xe0\0") + self.sock.close() + + def ping(self): + self.sock.write(b"\xc0\0") + + def publish(self, topic, msg, retain=False, qos=0): + pkt = bytearray(b"\x30\0\0\0") + pkt[0] |= qos << 1 | retain + sz = 2 + len(topic) + len(msg) + if qos > 0: + sz += 2 + assert sz < 2097152 + i = 1 + while sz > 0x7F: + pkt[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + pkt[i] = sz + # print(hex(len(pkt)), hexlify(pkt, ":")) + self.sock.write(pkt, i + 1) + self._send_str(topic) + if qos > 0: + self.pid += 1 + pid = self.pid + struct.pack_into("!H", pkt, 0, pid) + self.sock.write(pkt, 2) + self.sock.write(msg) + if qos == 1: + while 1: + op = self.wait_msg() + if op == 0x40: + sz = self.sock.read(1) + assert sz == b"\x02" + rcv_pid = self.sock.read(2) + rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] + if pid == rcv_pid: + return + elif qos == 2: + assert 0 + + def subscribe(self, topic, qos=0): + assert self.cb is not None, "Subscribe callback is not set" + pkt = bytearray(b"\x82\0\0\0") + self.pid += 1 + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) + # print(hex(len(pkt)), hexlify(pkt, ":")) + self.sock.write(pkt) + self._send_str(topic) + self.sock.write(qos.to_bytes(1, "little")) + while 1: + op = self.wait_msg() + if op == 0x90: + resp = self.sock.read(4) + # print(resp) + assert resp[1] == pkt[2] and resp[2] == pkt[3] + if resp[3] == 0x80: + raise MQTTException(resp[3]) + return + + # Wait for a single incoming MQTT message and process it. + # Subscribed messages are delivered to a callback previously + # set by .set_callback() method. Other (internal) MQTT + # messages processed internally. + def wait_msg(self): + res = self.sock.read(1) + self.sock.setblocking(True) + if res is None: + return None + if res == b"": + raise OSError(-1) + if res == b"\xd0": # PINGRESP + sz = self.sock.read(1)[0] + assert sz == 0 + return None + op = res[0] + if op & 0xF0 != 0x30: + return op + sz = self._recv_len() + topic_len = self.sock.read(2) + topic_len = (topic_len[0] << 8) | topic_len[1] + topic = self.sock.read(topic_len) + sz -= topic_len + 2 + if op & 6: + pid = self.sock.read(2) + pid = pid[0] << 8 | pid[1] + sz -= 2 + msg = self.sock.read(sz) + self.cb(topic, msg) + if op & 6 == 2: + pkt = bytearray(b"\x40\x02\0\0") + struct.pack_into("!H", pkt, 2, pid) + self.sock.write(pkt) + elif op & 6 == 4: + assert 0 + + # Checks whether a pending message from server is available. + # If not, returns immediately with None. Otherwise, does + # the same processing as wait_msg. + def check_msg(self): + self.sock.setblocking(False) + return self.wait_msg() diff --git a/scripts/lib/unique_id-1.0.1.dist-info/METADATA b/scripts/lib/unique_id-1.0.1.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..51ea95a7c245946842a53fcf02a4ac6d7c0df6c5 --- /dev/null +++ b/scripts/lib/unique_id-1.0.1.dist-info/METADATA @@ -0,0 +1,11 @@ +Metadata-Version: 2.1 +Name: unique-id +Version: 1.0.1 +Summary: Unique-ID is a small lib to generate unique ids - string values. +Home-page: +Download-URL: https://github.com/slawek87/unique-id +Author: Sławomir Kabik +Author-email: slawek@redsoftware.pl +Keywords: Python Unique ID,Python ID,Python Unique string +Requires-Dist: setuptools + diff --git a/scripts/lib/unique_id-1.0.1.dist-info/RECORD b/scripts/lib/unique_id-1.0.1.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..4e105c509b5ca6d69e6c95b968387ccc06a4d58a --- /dev/null +++ b/scripts/lib/unique_id-1.0.1.dist-info/RECORD @@ -0,0 +1,5 @@ +unique_id-1.0.1.dist-info/METADATA,, +unique_id/__init__.py,, +unique_id/main.py,, +unique_id/tests.py,, +unique_id-1.0.1.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/lib/unique_id/__init__.py b/scripts/lib/unique_id/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6138a1f801a012b90659605f39bbf723bd1b6397 --- /dev/null +++ b/scripts/lib/unique_id/__init__.py @@ -0,0 +1 @@ +from unique_id.main import get_unique_id \ No newline at end of file diff --git a/scripts/lib/unique_id/main.py b/scripts/lib/unique_id/main.py new file mode 100644 index 0000000000000000000000000000000000000000..87825a530e9c0ff6c467c569ff3c0305ca7dd5dd --- /dev/null +++ b/scripts/lib/unique_id/main.py @@ -0,0 +1,58 @@ +import random + + +class UniqueID(object): + """ + Generates Unique ID. + """ + DEFAULT_ID_LENGTH = 14 + DEFAULT_EXCLUDED_CHARS = ":*^`\",.~;%+-'" + + def __init__(self, length=DEFAULT_ID_LENGTH, excluded_chars=DEFAULT_EXCLUDED_CHARS): + """ + `length` - defines length of unique ID. + `excluded_chars` - defines chars excluded during generate process of unique ID. + """ + self.id_length = length + self.excluded_chars = excluded_chars + + def get_random_bits(self): + """ + Method returns random number included in max 8 bits. + """ + return random.getrandbits(8) + + def is_approved_ascii(self, ascii_number): + return 126 >= ascii_number >= 33 + + def is_excluded_char(self, current_char): + """ + Method checks if given char is not in excluded chars list. + """ + return current_char in self.excluded_chars + + def generate_id(self): + """ + Method generates unique ID. + """ + unique_id = "" + + while len(unique_id) < self.id_length: + ascii_number = self.get_random_bits() + + if self.is_approved_ascii(ascii_number): + random_char = chr(ascii_number) + + if not self.is_excluded_char(random_char): + unique_id += chr(ascii_number) + + return unique_id + + +def get_unique_id(length=UniqueID.DEFAULT_ID_LENGTH, excluded_chars=UniqueID.DEFAULT_EXCLUDED_CHARS): + """ + Function returns unique ID. + """ + unique_id = UniqueID(length=length, excluded_chars=excluded_chars) + return unique_id.generate_id() + diff --git a/scripts/lib/unique_id/tests.py b/scripts/lib/unique_id/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..6438c2cc99343d42b7d13616d88b3d87c41e2f61 --- /dev/null +++ b/scripts/lib/unique_id/tests.py @@ -0,0 +1,40 @@ +from random import randint +import unittest + +from unique_id import get_unique_id + + +class TestStringMethods(unittest.TestCase): + def test_unique_id(self): + unique_ids = list() + + for item in range(1000): + unique_id = get_unique_id() + + is_duplicated = unique_id in unique_ids + self.assertFalse(is_duplicated) + + unique_ids.append(unique_id) + + def test_max_length(self): + for item in range(1000): + id_length = randint(1, 128) + unique_id = get_unique_id(length=id_length) + + is_over_length = len(unique_id) != id_length + self.assertFalse(is_over_length) + + def test_excluded_chars(self): + id_length = 256 + excluded_chars = [1, 'f', 'm', 'a', 4, 5, 'Z', 'w', '_'] + + for item in range(1000): + unique_id = get_unique_id(length=id_length, excluded_chars=excluded_chars) + + for seed in unique_id: + is_excluded_char = seed in excluded_chars + self.assertFalse(is_excluded_char) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/lib/urequests_2.py b/scripts/lib/urequests_2.py new file mode 100644 index 0000000000000000000000000000000000000000..f43f88ebac649f9760537ed2de8c552a5107de55 --- /dev/null +++ b/scripts/lib/urequests_2.py @@ -0,0 +1,203 @@ +# Workaround for the `urequests` module to support HTTP/1.1 +# Based on https://github.com/micropython/micropython-lib/blob/e025c843b60e93689f0f991d753010bb5bd6a722/python-ecosys/requests/requests/__init__.py +# See https://github.com/micropython/micropython-lib/pull/861 and https://github.com/orgs/micropython/discussions/15112 +# `1.0` replaced with `1.1, i.e.: +# `s.write(b"%s /%s HTTP/1.0\r\n" % (method, path))` changed to `s.write(b"%s /%s HTTP/1.1\r\n" % (method, path))` +import usocket + + +class Response: + def __init__(self, f): + self.raw = f + self.encoding = "utf-8" + self._cached = None + + def close(self): + if self.raw: + self.raw.close() + self.raw = None + self._cached = None + + @property + def content(self): + if self._cached is None: + try: + self._cached = self.raw.read() + finally: + self.raw.close() + self.raw = None + return self._cached + + @property + def text(self): + return str(self.content, self.encoding) + + def json(self): + import ujson + + return ujson.loads(self.content) + + +def request( + method, + url, + data=None, + json=None, + headers={}, + stream=None, + auth=None, + timeout=None, + parse_headers=True, +): + redirect = None # redirection url, None means no redirection + chunked_data = ( + data and getattr(data, "__next__", None) and not getattr(data, "__len__", None) + ) + + if auth is not None: + import ubinascii + + username, password = auth + formated = b"{}:{}".format(username, password) + formated = str(ubinascii.b2a_base64(formated)[:-1], "ascii") + headers["Authorization"] = "Basic {}".format(formated) + + try: + proto, dummy, host, path = url.split("/", 3) + except ValueError: + proto, dummy, host = url.split("/", 2) + path = "" + if proto == "http:": + port = 80 + elif proto == "https:": + import ussl + + port = 443 + else: + raise ValueError("Unsupported protocol: " + proto) + + if ":" in host: + host, port = host.split(":", 1) + port = int(port) + + ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM) + ai = ai[0] + + resp_d = None + if parse_headers is not False: + resp_d = {} + + s = usocket.socket(ai[0], usocket.SOCK_STREAM, ai[2]) + + if timeout is not None: + # Note: settimeout is not supported on all platforms, will raise + # an AttributeError if not available. + s.settimeout(timeout) + + try: + s.connect(ai[-1]) + if proto == "https:": + s = ussl.wrap_socket(s, server_hostname=host) + s.write(b"%s /%s HTTP/1.1\r\n" % (method, path)) + if "Host" not in headers: + s.write(b"Host: %s\r\n" % host) + # Iterate over keys to avoid tuple alloc + for k in headers: + s.write(k) + s.write(b": ") + s.write(headers[k]) + s.write(b"\r\n") + if json is not None: + assert data is None + import ujson + + data = ujson.dumps(json) + s.write(b"Content-Type: application/json\r\n") + if data: + if chunked_data: + s.write(b"Transfer-Encoding: chunked\r\n") + else: + s.write(b"Content-Length: %d\r\n" % len(data)) + s.write(b"Connection: close\r\n\r\n") + if data: + if chunked_data: + for chunk in data: + s.write(b"%x\r\n" % len(chunk)) + s.write(chunk) + s.write(b"\r\n") + s.write("0\r\n\r\n") + else: + s.write(data) + + l = s.readline() + # print(l) + l = l.split(None, 2) + if len(l) < 2: + # Invalid response + raise ValueError("HTTP error: BadStatusLine:\n%s" % l) + status = int(l[1]) + reason = "" + if len(l) > 2: + reason = l[2].rstrip() + while True: + l = s.readline() + if not l or l == b"\r\n": + break + # print(l) + if l.startswith(b"Transfer-Encoding:"): + if b"chunked" in l: + raise ValueError("Unsupported " + str(l, "utf-8")) + elif l.startswith(b"Location:") and not 200 <= status <= 299: + if status in [301, 302, 303, 307, 308]: + redirect = str(l[10:-2], "utf-8") + else: + raise NotImplementedError("Redirect %d not yet supported" % status) + if parse_headers is False: + pass + elif parse_headers is True: + l = str(l, "utf-8") + k, v = l.split(":", 1) + resp_d[k] = v.strip() + else: + parse_headers(l, resp_d) + except OSError: + s.close() + raise + + if redirect: + s.close() + if status in [301, 302, 303]: + return request("GET", redirect, None, None, headers, stream) + else: + return request(method, redirect, data, json, headers, stream) + else: + resp = Response(s) + resp.status_code = status + resp.reason = reason + if resp_d is not None: + resp.headers = resp_d + return resp + + +def head(url, **kw): + return request("HEAD", url, **kw) + + +def get(url, **kw): + return request("GET", url, **kw) + + +def post(url, **kw): + return request("POST", url, **kw) + + +def put(url, **kw): + return request("PUT", url, **kw) + + +def patch(url, **kw): + return request("PATCH", url, **kw) + + +def delete(url, **kw): + return request("DELETE", url, **kw) diff --git a/scripts/pico_id.txt b/scripts/pico_id.txt new file mode 100644 index 0000000000000000000000000000000000000000..3bb8d27d25a78a0a0553101c8232d53b48241dff --- /dev/null +++ b/scripts/pico_id.txt @@ -0,0 +1 @@ +e66130100f594628 \ No newline at end of file diff --git a/scripts/temperature-sensor.py b/scripts/temperature-sensor.py new file mode 100644 index 0000000000000000000000000000000000000000..729d7eb812818976cbf87846e675ac3fccdea9cb --- /dev/null +++ b/scripts/temperature-sensor.py @@ -0,0 +1,83 @@ +from bme680 import BME680_I2C # Ensure you have the right import for the BME680 class +from machine import I2C, Pin +from netman import connectWiFi +from umqtt.simple import MQTTClient +import time + +# Wi-Fi and MQTT configuration +SSID = 'Pixel 8' # Replace with your Wi-Fi SSID +PASSWORD = '123456789' # Replace with your Wi-Fi password +MQTT_BROKER = 'b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud' # HiveMQ Cloud broker URL +MQTT_PORT = 8883 # Port for TLS +MQTT_TOPIC = "sensors/bme680/data" # Replace with your desired MQTT topic + +MQTT_USER = 'Luthiraa' +MQTT_PASS = 'theboss1010' + +def connect_to_internet(): + try: + status = connectWiFi(SSID, PASSWORD, country='US', retries=3) + print("Connected to Wi-Fi successfully!") + print("IP Address:", status[0]) + except RuntimeError as e: + print(f"Failed to connect to Wi-Fi: {e}") + raise + +# Initialize I2C and BME680 +i2c = I2C(1, scl=Pin(27), sda=Pin(26)) +bme = BME680_I2C(i2c) + +# MQTT setup with authentication and TLS +client = MQTTClient( + client_id=b"kudzai_raspberrypi_picow", + server=MQTT_BROKER, + port=MQTT_PORT, + user=MQTT_USER, + password=MQTT_PASS, + keepalive=60, # Set to a shorter interval + ssl=True, + ssl_params={'server_hostname': MQTT_BROKER} +) +# Connect to MQTT broker +def connect_to_mqtt(): + try: + client.connect() + print("Connected to MQTT broker") + print("Client ID:", client.client_id) # Print client ID + except Exception as e: + print(f"Failed to connect to MQTT broker: {e}") + raise + +# Connect to Wi-Fi and MQTT +connect_to_internet() +connect_to_mqtt() + +while True: + # Read sensor data + temperature = bme.temperature + humidity = bme.humidity + pressure = bme.pressure + gas = bme.gas + + # Prepare data payload + payload = ( + f"Temperature: {temperature:.2f} °C, " + f"Humidity: {humidity:.2f} %, " + f"Pressure: {pressure:.2f} hPa, " + f"Gas: {gas:.2f} ohms" + ) + + # Print data to console + print("--------------------------------------------------") + print(payload) + print("--------------------------------------------------") + + # Publish data to MQTT broker + try: + client.publish(MQTT_TOPIC, payload) + print("Data published to MQTT topic:", MQTT_TOPIC) + except Exception as e: + print(f"Failed to publish data: {e}") + client.connect() + + time.sleep(2)